diff --git a/.github/release.yml b/.github/release.yml new file mode 100644 index 000000000..e29eb8464 --- /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 56a7e4fa6..000000000 --- a/.github/workflows/ci.yaml +++ /dev/null @@ -1,159 +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: | - ./scripts/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" - - 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: - 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' - 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 - 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/main.yml b/.github/workflows/main.yml new file mode 100644 index 000000000..abff4d751 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,32 @@ +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 -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_2_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 + 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 + + 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 new file mode 100644 index 000000000..ca07e7dc8 --- /dev/null +++ b/.github/workflows/pull_request.yml @@ -0,0 +1,74 @@ +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-availability" + linux_6_1_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-availability" + linux_6_2_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" + + 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: "apt update && apt install -y protobuf-compiler" + + examples-matrix: + name: Examples + needs: construct-examples-matrix + uses: apple/swift-nio/.github/workflows/swift_test_matrix.yml@main + with: + name: "Examples" + matrix_string: '${{ needs.construct-examples-matrix.outputs.examples-matrix }}' + + 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 + + static-sdk: + name: Static SDK + uses: apple/swift-nio/.github/workflows/static_sdk.yml@main 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/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/.github/workflows/soundness.yml b/.github/workflows/soundness.yml new file mode 100644 index 000000000..cd3f196aa --- /dev/null +++ b/.github/workflows/soundness.yml @@ -0,0 +1,51 @@ +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 + + 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/.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..6a74dfa65 --- /dev/null +++ b/.licenseignore @@ -0,0 +1,43 @@ +.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 +**/*.pb 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/.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/AUTHORS b/AUTHORS deleted file mode 100644 index e491a9e7f..000000000 --- a/AUTHORS +++ /dev/null @@ -1 +0,0 @@ -Google Inc. 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/FuzzTesting/.gitignore b/Examples/echo-metadata/.gitignore similarity index 71% rename from FuzzTesting/.gitignore rename to Examples/echo-metadata/.gitignore index bb460e7be..0023a5340 100644 --- a/FuzzTesting/.gitignore +++ b/Examples/echo-metadata/.gitignore @@ -1,7 +1,8 @@ .DS_Store /.build /Packages -/*.xcodeproj 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..e19ab6ef0 --- /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", 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: [ + .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/Tests/GRPCHTTP2CoreTests/Test Utilities/MethodDescriptor+Common.swift b/Examples/echo-metadata/Sources/ClientArguments.swift similarity index 53% rename from Tests/GRPCHTTP2CoreTests/Test Utilities/MethodDescriptor+Common.swift rename to Examples/echo-metadata/Sources/ClientArguments.swift index e7c1ef50a..9aa237f67 100644 --- a/Tests/GRPCHTTP2CoreTests/Test Utilities/MethodDescriptor+Common.swift +++ b/Examples/echo-metadata/Sources/ClientArguments.swift @@ -1,5 +1,5 @@ /* - * Copyright 2024, gRPC Authors All rights reserved. + * 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. @@ -14,21 +14,22 @@ * limitations under the License. */ -import GRPCCore +import ArgumentParser +import GRPCNIOTransportHTTP2 -extension MethodDescriptor { - static var echoGet: Self { - MethodDescriptor(service: "echo.Echo", method: "Get") - } +struct ClientArguments: ParsableArguments { + @Option(help: "The server's listening port") + var port: Int = 1234 - static var echoUpdate: Self { - MethodDescriptor(service: "echo.Echo", method: "Update") - } + @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 } -@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) +extension ClientArguments { + var target: any ResolvableTarget { + return .ipv4(host: "127.0.0.1", port: self.port) } } diff --git a/Sources/GRPC/ConnectionPool/StreamLender.swift b/Examples/echo-metadata/Sources/EchoMetadata.swift similarity index 58% rename from Sources/GRPC/ConnectionPool/StreamLender.swift rename to Examples/echo-metadata/Sources/EchoMetadata.swift index cdd507f73..4624f16c7 100644 --- a/Sources/GRPC/ConnectionPool/StreamLender.swift +++ b/Examples/echo-metadata/Sources/EchoMetadata.swift @@ -1,5 +1,5 @@ /* - * Copyright 2021, gRPC Authors All rights reserved. + * 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. @@ -14,11 +14,14 @@ * limitations under the License. */ -@usableFromInline -internal protocol StreamLender { - /// `count` streams are being returned to the given `pool`. - func returnStreams(_ count: Int, to pool: ConnectionPool) +import ArgumentParser +import GRPCCore - /// 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) +@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/.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..6705a8b1f --- /dev/null +++ b/Examples/echo/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", + platforms: [.macOS("15.0")], + 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-nio-transport.git", from: "1.0.0"), + .package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.5.0"), + ], + targets: [ + .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"), + ], + plugins: [ + .plugin(name: "GRPCProtobufGenerator", package: "grpc-swift-protobuf") + ] + ) + ] +) diff --git a/Examples/echo/README.md b/Examples/echo/README.md new file mode 100644 index 000000000..7753d3e1e --- /dev/null +++ b/Examples/echo/README.md @@ -0,0 +1,58 @@ +# Echo + +This example demonstrates all four RPC types using a simple 'echo' service and +client and the SwiftNIO 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. + +## 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 serve +Echo listening on [ipv4]127.0.0.1:1234 +``` + +Use the CLI to make a unary 'Get' request against it: + +```console +$ PROTOC_PATH=$(which protoc) swift run echo get --message "Hello" +get โ†’ Hello +get โ† Hello +``` + +Use the CLI to make a bidirectional streaming 'Update' request: + +```console +$ PROTOC_PATH=$(which protoc) swift run echo update --message "Hello World" +update โ†’ Hello +update โ†’ World +update โ† Hello +update โ† World +``` + +Get help with the CLI by running: + +```console +$ 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/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/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/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 73% rename from Examples/v2/echo/Subcommands/Collect.swift rename to Examples/echo/Sources/Subcommands/Collect.swift index 3a61915df..27350774a 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." @@ -29,19 +27,13 @@ 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, - config: .defaults(transportSecurity: .plaintext) + transportSecurity: .plaintext ) - ) - - try await withThrowingDiscardingTaskGroup { group in - group.addTask { - try await client.run() - } - - let echo = Echo_EchoClient(wrapping: client) + ) { client in + let echo = Echo_Echo.Client(wrapping: client) for _ in 0 ..< self.arguments.repetitions { let message = try await echo.collect { writer in @@ -52,8 +44,6 @@ struct Collect: AsyncParsableCommand { } print("collect โ† \(message.text)") } - - client.beginGracefulShutdown() } } } 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/v2/echo/Subcommands/Expand.swift b/Examples/echo/Sources/Subcommands/Expand.swift similarity index 73% rename from Examples/v2/echo/Subcommands/Expand.swift rename to Examples/echo/Sources/Subcommands/Expand.swift index 1d06bdd99..b488c9977 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." @@ -29,19 +27,13 @@ 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, - config: .defaults(transportSecurity: .plaintext) + transportSecurity: .plaintext ) - ) - - try await withThrowingDiscardingTaskGroup { group in - group.addTask { - try await client.run() - } - - let echo = Echo_EchoClient(wrapping: client) + ) { client in + let echo = Echo_Echo.Client(wrapping: client) for _ in 0 ..< self.arguments.repetitions { let message = Echo_EchoRequest.with { $0.text = self.arguments.message } @@ -52,8 +44,6 @@ struct Expand: AsyncParsableCommand { } } } - - client.beginGracefulShutdown() } } } diff --git a/Examples/v2/echo/Subcommands/Get.swift b/Examples/echo/Sources/Subcommands/Get.swift similarity index 72% rename from Examples/v2/echo/Subcommands/Get.swift rename to Examples/echo/Sources/Subcommands/Get.swift index 0dd551002..4c429910c 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.") @@ -27,19 +25,13 @@ 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, - config: .defaults(transportSecurity: .plaintext) + transportSecurity: .plaintext ) - ) - - try await withThrowingDiscardingTaskGroup { group in - group.addTask { - try await client.run() - } - - let echo = Echo_EchoClient(wrapping: client) + ) { client in + let echo = Echo_Echo.Client(wrapping: client) for _ in 0 ..< self.arguments.repetitions { let message = Echo_EchoRequest.with { $0.text = self.arguments.message } @@ -47,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/Serve.swift b/Examples/echo/Sources/Subcommands/Serve.swift new file mode 100644 index 000000000..17f38aec7 --- /dev/null +++ b/Examples/echo/Sources/Subcommands/Serve.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 ArgumentParser +import GRPCCore +import GRPCNIOTransportHTTP2 + +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 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 listening on \(address)") + } + } + } +} diff --git a/Examples/v2/echo/Subcommands/Update.swift b/Examples/echo/Sources/Subcommands/Update.swift similarity index 75% rename from Examples/v2/echo/Subcommands/Update.swift rename to Examples/echo/Sources/Subcommands/Update.swift index 1c189caa8..726e8b83d 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." @@ -29,19 +27,13 @@ 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, - config: .defaults(transportSecurity: .plaintext) + transportSecurity: .plaintext ) - ) - - try await withThrowingDiscardingTaskGroup { group in - group.addTask { - try await client.run() - } - - let echo = Echo_EchoClient(wrapping: client) + ) { client in + let echo = Echo_Echo.Client(wrapping: client) for _ in 0 ..< self.arguments.repetitions { try await echo.update { writer in @@ -55,8 +47,6 @@ struct Update: AsyncParsableCommand { } } } - - client.beginGracefulShutdown() } } } 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..b2f15c721 --- /dev/null +++ b/Examples/error-details/Package.swift @@ -0,0 +1,40 @@ +// 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", from: "2.0.0"), + .package(url: "https://github.com/grpc/grpc-swift-protobuf.git", from: "1.0.0"), + ], + 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"), + ], + plugins: [ + .plugin(name: "GRPCProtobufGenerator", package: "grpc-swift-protobuf") + ] + ) + ] +) diff --git a/Examples/error-details/README.md b/Examples/error-details/README.md new file mode 100644 index 000000000..6dc09059e --- /dev/null +++ b/Examples/error-details/README.md @@ -0,0 +1,38 @@ +# 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. + +## 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 +$ PROTOC_PATH=$(which protoc) 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.) +``` + +[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/Sources/DetailedErrorExample.swift b/Examples/error-details/Sources/DetailedErrorExample.swift new file mode 100644 index 000000000..f4bfa464c --- /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/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/.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..daa71a571 --- /dev/null +++ b/Examples/hello-world/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: "hello-world", + platforms: [.macOS("15.0")], + 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-nio-transport.git", from: "1.0.0"), + .package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.5.0"), + ], + targets: [ + .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"), + ], + plugins: [ + .plugin(name: "GRPCProtobufGenerator", package: "grpc-swift-protobuf") + ] + ) + ] +) diff --git a/Examples/hello-world/README.md b/Examples/hello-world/README.md new file mode 100644 index 000000000..32fa4ca5a --- /dev/null +++ b/Examples/hello-world/README.md @@ -0,0 +1,46 @@ +# 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. + +## 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 hello-world serve +Greeter listening on [ipv4]127.0.0.1:31415 +``` + +Use the CLI to send a request to the service: + +```console +$ 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 +$ 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/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/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/v2/hello-world/Subcommands/Greet.swift b/Examples/hello-world/Sources/Subcommands/Greet.swift similarity index 66% rename from Examples/v2/hello-world/Subcommands/Greet.swift rename to Examples/hello-world/Sources/Subcommands/Greet.swift index 069b8faee..643c90798 100644 --- a/Examples/v2/hello-world/Subcommands/Greet.swift +++ b/Examples/hello-world/Sources/Subcommands/Greet.swift @@ -15,10 +15,10 @@ */ import ArgumentParser -import GRPCHTTP2Transport +import GRPCCore +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") @@ -29,23 +29,13 @@ 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), - config: .defaults(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() - } - - let greeter = Helloworld_GreeterClient(wrapping: client) + ) { 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/v2/hello-world/Subcommands/Serve.swift b/Examples/hello-world/Sources/Subcommands/Serve.swift similarity index 72% rename from Examples/v2/hello-world/Subcommands/Serve.swift rename to Examples/hello-world/Sources/Subcommands/Serve.swift index a9dd178ec..caf801ae4 100644 --- a/Examples/v2/hello-world/Subcommands/Serve.swift +++ b/Examples/hello-world/Sources/Subcommands/Serve.swift @@ -15,10 +15,10 @@ */ import ArgumentParser -import GRPCHTTP2Transport +import GRPCCore +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.") @@ -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,15 +43,14 @@ struct Serve: AsyncParsableCommand { } } -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -struct Greeter: Helloworld_GreeterServiceProtocol { +struct Greeter: Helloworld_Greeter.SimpleServiceProtocol { func sayHello( - request: ServerRequest.Single, + request: Helloworld_HelloRequest, context: ServerContext - ) async throws -> ServerResponse.Single { + ) 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.Single(message: reply) + return reply } } 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..4361c689c --- /dev/null +++ b/Examples/reflection-server/Package.swift @@ -0,0 +1,48 @@ +// 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", 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: [ + .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") + ], + plugins: [ + .plugin(name: "GRPCProtobufGenerator", package: "grpc-swift-protobuf") + ] + ) + ] +) diff --git a/Examples/reflection-server/README.md b/Examples/reflection-server/README.md new file mode 100644 index 000000000..0954d844d --- /dev/null +++ b/Examples/reflection-server/README.md @@ -0,0 +1,73 @@ +# 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 +``` + +## 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 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" +} +``` + +[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/Sources/DescriptorSets/echo.pb b/Examples/reflection-server/Sources/DescriptorSets/echo.pb new file mode 100644 index 000000000..dce4d22e3 Binary files /dev/null and b/Examples/reflection-server/Sources/DescriptorSets/echo.pb differ 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/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/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/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..b4a9589f1 --- /dev/null +++ b/Examples/route-guide/Package.swift @@ -0,0 +1,46 @@ +// 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.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: [ + .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"), + ], + resources: [ + .copy("route_guide_db.json") + ], + plugins: [ + .plugin(name: "GRPCProtobufGenerator", package: "grpc-swift-protobuf") + ] + ) + ] +) diff --git a/Examples/route-guide/README.md b/Examples/route-guide/README.md new file mode 100644 index 000000000..8a06ed735 --- /dev/null +++ b/Examples/route-guide/README.md @@ -0,0 +1,56 @@ +# 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). + +## 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 route-guide serve +server listening on [ipv4]127.0.0.1:31415 +``` + +Use the CLI to interrogate the different RPCs you can call: + +```console +$ PROTOC_PATH=$(which protoc) 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. +``` + +[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/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/Examples/v2/route-guide/RouteGuide.swift b/Examples/route-guide/Sources/RouteGuide.swift similarity index 92% rename from Examples/v2/route-guide/RouteGuide.swift rename to Examples/route-guide/Sources/RouteGuide.swift index d53882726..425fe2103 100644 --- a/Examples/v2/route-guide/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/Examples/v2/route-guide/Subcommands/GetFeature.swift b/Examples/route-guide/Sources/Subcommands/GetFeature.swift similarity index 75% rename from Examples/v2/route-guide/Subcommands/GetFeature.swift rename to Examples/route-guide/Sources/Subcommands/GetFeature.swift index 1c42ec6da..6ea6a12bd 100644 --- a/Examples/v2/route-guide/Subcommands/GetFeature.swift +++ b/Examples/route-guide/Sources/Subcommands/GetFeature.swift @@ -15,9 +15,9 @@ */ import ArgumentParser -import GRPCHTTP2Transport +import GRPCCore +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.") @@ -37,18 +37,13 @@ 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), - config: .defaults(transportSecurity: .plaintext) - ) - let client = GRPCClient(transport: transport) - - try await withThrowingDiscardingTaskGroup { group in - group.addTask { - try await client.run() - } - - let routeGuide = Routeguide_RouteGuideClient(wrapping: client) + 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 { $0.latitude = self.latitude @@ -62,8 +57,6 @@ struct GetFeature: AsyncParsableCommand { } else { print("Found '\(feature.name)' at (\(self.latitude), \(self.longitude))") } - - client.beginGracefulShutdown() } } } diff --git a/Examples/v2/route-guide/Subcommands/ListFeatures.swift b/Examples/route-guide/Sources/Subcommands/ListFeatures.swift similarity index 80% rename from Examples/v2/route-guide/Subcommands/ListFeatures.swift rename to Examples/route-guide/Sources/Subcommands/ListFeatures.swift index 887a944e2..be6d754dc 100644 --- a/Examples/v2/route-guide/Subcommands/ListFeatures.swift +++ b/Examples/route-guide/Sources/Subcommands/ListFeatures.swift @@ -15,9 +15,9 @@ */ import ArgumentParser -import GRPCHTTP2Transport +import GRPCCore +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." @@ -51,18 +51,13 @@ 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), - config: .defaults(transportSecurity: .plaintext) - ) - let client = GRPCClient(transport: transport) - - try await withThrowingDiscardingTaskGroup { group in - group.addTask { - try await client.run() - } - - let routeGuide = Routeguide_RouteGuideClient(wrapping: client) + 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 $0.hi.latitude = self.maxLatitude @@ -78,9 +73,6 @@ struct ListFeatures: AsyncParsableCommand { print("(\(count)) \(feature.name) at (\(lat), \(lon))") } } - - client.beginGracefulShutdown() } - } } diff --git a/Examples/v2/route-guide/Subcommands/RecordRoute.swift b/Examples/route-guide/Sources/Subcommands/RecordRoute.swift similarity index 77% rename from Examples/v2/route-guide/Subcommands/RecordRoute.swift rename to Examples/route-guide/Sources/Subcommands/RecordRoute.swift index cd443230b..7a652206c 100644 --- a/Examples/v2/route-guide/Subcommands/RecordRoute.swift +++ b/Examples/route-guide/Sources/Subcommands/RecordRoute.swift @@ -15,9 +15,9 @@ */ import ArgumentParser -import GRPCHTTP2Transport +import GRPCCore +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." @@ -30,18 +30,13 @@ 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), - config: .defaults(transportSecurity: .plaintext) - ) - let client = GRPCClient(transport: transport) - - try await withThrowingDiscardingTaskGroup { group in - group.addTask { - try await client.run() - } - - let routeGuide = Routeguide_RouteGuideClient(wrapping: client) + 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. let rectangle = Routeguide_Rectangle.with { @@ -67,8 +62,6 @@ struct RecordRoute: AsyncParsableCommand { a distance \(summary.distance) metres. """ print(text) - - client.beginGracefulShutdown() } } } diff --git a/Examples/v2/route-guide/Subcommands/RouteChat.swift b/Examples/route-guide/Sources/Subcommands/RouteChat.swift similarity index 77% rename from Examples/v2/route-guide/Subcommands/RouteChat.swift rename to Examples/route-guide/Sources/Subcommands/RouteChat.swift index 81cb5c3e2..ba6d92424 100644 --- a/Examples/v2/route-guide/Subcommands/RouteChat.swift +++ b/Examples/route-guide/Sources/Subcommands/RouteChat.swift @@ -15,9 +15,9 @@ */ import ArgumentParser -import GRPCHTTP2Transport +import GRPCCore +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: """ @@ -30,18 +30,13 @@ 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), - config: .defaults(transportSecurity: .plaintext) - ) - let client = GRPCClient(transport: transport) - - try await withThrowingDiscardingTaskGroup { group in - group.addTask { - try await client.run() - } - - let routeGuide = Routeguide_RouteGuideClient(wrapping: client) + 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 let notes: [(String, (Int32, Int32))] = [ @@ -67,8 +62,6 @@ struct RouteChat: AsyncParsableCommand { print("Received note: '\(note.message) at (\(lat), \(lon))'") } } - - client.beginGracefulShutdown() } } } diff --git a/Examples/v2/route-guide/Subcommands/Serve.swift b/Examples/route-guide/Sources/Subcommands/Serve.swift similarity index 78% rename from Examples/v2/route-guide/Subcommands/Serve.swift rename to Examples/route-guide/Sources/Subcommands/Serve.swift index f9b942876..066d8e396 100644 --- a/Examples/v2/route-guide/Subcommands/Serve.swift +++ b/Examples/route-guide/Sources/Subcommands/Serve.swift @@ -16,11 +16,11 @@ import ArgumentParser import Foundation -import GRPCHTTP2Transport +import GRPCCore +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.") @@ -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)]) @@ -52,7 +52,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,57 +99,54 @@ struct RouteGuideService { } } -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -extension RouteGuideService: Routeguide_RouteGuide.ServiceProtocol { +extension RouteGuideService: Routeguide_RouteGuide.SimpleServiceProtocol { func getFeature( - request: ServerRequest.Single, + request: Routeguide_Point, context: ServerContext - ) async throws -> ServerResponse.Single { + ) 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.Single(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.Single(message: unknownFeature) + return unknownFeature } } func listFeatures( - request: ServerRequest.Single, + request: Routeguide_Rectangle, + response: RPCWriter, context: ServerContext - ) 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 [:] + ) 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: ServerRequest.Stream, + request: RPCAsyncSequence, context: ServerContext - ) async throws -> ServerResponse.Single { + ) 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 { @@ -172,19 +168,17 @@ extension RouteGuideService: Routeguide_RouteGuide.ServiceProtocol { $0.distance = Int32(distanceTravelled) } - return ServerResponse.Single(message: summary) + return summary } func routeChat( - request: ServerRequest.Stream, + request: RPCAsyncSequence, + response: RPCWriter, context: ServerContext - ) 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 [:] + ) async throws { + for try await note in request { + let notes = self.receivedNotes.recordNote(note) + try await response.write(contentsOf: notes) } } } @@ -223,8 +217,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/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..1f69fc4db --- /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! +Bonjour, 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..56f96c0f4 --- /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", + "Bonjour", + "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 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/Examples/v2/echo/Generated/echo.grpc.swift b/Examples/v2/echo/Generated/echo.grpc.swift deleted file mode 100644 index 63a264205..000000000 --- a/Examples/v2/echo/Generated/echo.grpc.swift +++ /dev/null @@ -1,493 +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 - -internal enum 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 = GRPCCore.MethodDescriptor( - service: Echo_Echo.descriptor.fullyQualifiedService, - method: "Get" - ) - } - internal enum Expand { - internal typealias Input = Echo_EchoRequest - internal typealias Output = Echo_EchoResponse - internal static let descriptor = GRPCCore.MethodDescriptor( - service: Echo_Echo.descriptor.fullyQualifiedService, - method: "Expand" - ) - } - internal enum Collect { - internal typealias Input = Echo_EchoRequest - internal typealias Output = Echo_EchoResponse - internal static let descriptor = GRPCCore.MethodDescriptor( - service: Echo_Echo.descriptor.fullyQualifiedService, - method: "Collect" - ) - } - internal enum Update { - internal typealias Input = Echo_EchoRequest - internal typealias Output = Echo_EchoResponse - internal static let descriptor = GRPCCore.MethodDescriptor( - service: Echo_Echo.descriptor.fullyQualifiedService, - method: "Update" - ) - } - internal static let descriptors: [GRPCCore.MethodDescriptor] = [ - Get.descriptor, - Expand.descriptor, - Collect.descriptor, - Update.descriptor - ] - } - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - internal typealias StreamingServiceProtocol = Echo_EchoStreamingServiceProtocol - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - internal typealias ServiceProtocol = Echo_EchoServiceProtocol - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - internal typealias ClientProtocol = Echo_EchoClientProtocol - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - internal typealias Client = Echo_EchoClient -} - -extension GRPCCore.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. - 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, - 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, - 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, - 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 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, - 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 - ) - } - ) - } -} - -@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, - 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, - 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, - 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, - 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, - 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, - 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, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse.Stream { - let response = try await self.collect( - request: request, - context: context - ) - return GRPCCore.ServerResponse.Stream(single: response) - } -} - -@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( - 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: 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: 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: 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: 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: GRPCProtobuf.ProtobufSerializer(), - deserializer: GRPCProtobuf.ProtobufDeserializer(), - options: options, - body - ) - } - - internal func expand( - 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: GRPCProtobuf.ProtobufSerializer(), - deserializer: GRPCProtobuf.ProtobufDeserializer(), - options: options, - body - ) - } - - internal func collect( - 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: GRPCProtobuf.ProtobufSerializer(), - deserializer: GRPCProtobuf.ProtobufDeserializer(), - options: options, - body - ) - } - - internal func update( - 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: 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 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 - - internal init(wrapping client: GRPCCore.GRPCClient) { - self.client = client - } - - /// Immediately returns an echo of a request. - internal func get( - 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: 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.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: 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.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: 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.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: Echo_Echo.Method.Update.descriptor, - serializer: serializer, - deserializer: deserializer, - options: options, - handler: body - ) - } -} \ No newline at end of file diff --git a/Examples/v2/echo/Generated/echo.pb.swift b/Examples/v2/echo/Generated/echo.pb.swift deleted file mode 100644 index 88ef21ca9..000000000 --- a/Examples/v2/echo/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/v2/echo/Subcommands/Serve.swift b/Examples/v2/echo/Subcommands/Serve.swift deleted file mode 100644 index 5bfa1772f..000000000 --- a/Examples/v2/echo/Subcommands/Serve.swift +++ /dev/null @@ -1,88 +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 - -@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.") - - @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), - config: .defaults(transportSecurity: .plaintext) - ), - services: [EchoService()] - ) - - try await withThrowingDiscardingTaskGroup { group in - group.addTask { try await server.serve() } - if let address = try await server.listeningAddress { - print("Echo listening on \(address)") - } - } - } -} - -@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, - context: ServerContext - ) async throws -> ServerResponse.Single { - return ServerResponse.Single(message: .with { $0.text = request.message.text }) - } - - func collect( - 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: " ") - return ServerResponse.Single(message: .with { $0.text = joined }) - } - - func expand( - request: ServerRequest.Single, - context: ServerContext - ) 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, - context: ServerContext - ) 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/Examples/v2/hello-world/Generated/helloworld.grpc.swift b/Examples/v2/hello-world/Generated/helloworld.grpc.swift deleted file mode 100644 index 8044411e5..000000000 --- a/Examples/v2/hello-world/Generated/helloworld.grpc.swift +++ /dev/null @@ -1,196 +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 - -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, - 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, *) - 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 - ) - } - ) - } -} - -/// 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, - 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, - 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, *) -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/Examples/v2/hello-world/Generated/helloworld.pb.swift b/Examples/v2/hello-world/Generated/helloworld.pb.swift deleted file mode 100644 index 20b4f36df..000000000 --- a/Examples/v2/hello-world/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/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/Examples/v2/route-guide/Generated/route_guide.grpc.swift b/Examples/v2/route-guide/Generated/route_guide.grpc.swift deleted file mode 100644 index 6a89ed2a3..000000000 --- a/Examples/v2/route-guide/Generated/route_guide.grpc.swift +++ /dev/null @@ -1,577 +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 - -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, - context: GRPCCore.ServerContext - ) 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, - 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, - 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, - 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 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, 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 - ) - } - ) - } -} - -/// 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, - context: GRPCCore.ServerContext - ) 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, - 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, - 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, - 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, - 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, - 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, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse.Stream { - let response = try await self.recordRoute( - request: request, - context: context - ) - 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 deleted file mode 100644 index 09892ef83..000000000 --- a/Examples/v2/route-guide/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/FuzzTesting/FailCases/clusterfuzz-testcase-minimized-ServerFuzzer-release-4739158818553856 b/FuzzTesting/FailCases/clusterfuzz-testcase-minimized-ServerFuzzer-release-4739158818553856 deleted file mode 100644 index e7972e42c..000000000 Binary files a/FuzzTesting/FailCases/clusterfuzz-testcase-minimized-ServerFuzzer-release-4739158818553856 and /dev/null differ diff --git a/FuzzTesting/FailCases/clusterfuzz-testcase-minimized-ServerFuzzer-release-5077460227063808 b/FuzzTesting/FailCases/clusterfuzz-testcase-minimized-ServerFuzzer-release-5077460227063808 deleted file mode 100644 index 1d3717fa6..000000000 Binary files a/FuzzTesting/FailCases/clusterfuzz-testcase-minimized-ServerFuzzer-release-5077460227063808 and /dev/null differ 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 4b351f2d4..000000000 Binary files a/FuzzTesting/FailCases/clusterfuzz-testcase-minimized-ServerFuzzer-release-5134158417494016 and /dev/null differ diff --git a/FuzzTesting/FailCases/clusterfuzz-testcase-minimized-ServerFuzzer-release-5285159577452544 b/FuzzTesting/FailCases/clusterfuzz-testcase-minimized-ServerFuzzer-release-5285159577452544 deleted file mode 100644 index e6d890818..000000000 --- a/FuzzTesting/FailCases/clusterfuzz-testcase-minimized-ServerFuzzer-release-5285159577452544 +++ /dev/null @@ -1,10 +0,0 @@ -PUT /echo.Echo/Collect HTTP/1.1 -Content-Type:application/grpc -transfer-encoding:cHUnked - -3 - - - -PUT * HTTP/1.1 - diff --git a/FuzzTesting/FailCases/clusterfuzz-testcase-minimized-ServerFuzzer-release-5448955772141568 b/FuzzTesting/FailCases/clusterfuzz-testcase-minimized-ServerFuzzer-release-5448955772141568 deleted file mode 100644 index c0186cf1c..000000000 Binary files a/FuzzTesting/FailCases/clusterfuzz-testcase-minimized-ServerFuzzer-release-5448955772141568 and /dev/null differ 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 51% rename from Performance/Benchmarks/Package.swift rename to IntegrationTests/Benchmarks/Package.swift index 2c1fe63ab..3fe8b8aed 100644 --- a/Performance/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/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/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 new file mode 100644 index 000000000..9c403f41f --- /dev/null +++ b/IntegrationTests/Benchmarks/Thresholds/6.0/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_strings_stored.p90.json @@ -0,0 +1,3 @@ +{ + "mallocCountTotal": 1000 +} 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/6.1/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/6.1/GRPCSwiftBenchmark.Metadata_Add_binary.p90.json diff --git a/Performance/Benchmarks/Thresholds/main/GRPCSwiftBenchmark.Metadata_Add_string.p90.json b/IntegrationTests/Benchmarks/Thresholds/6.1/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/6.1/GRPCSwiftBenchmark.Metadata_Add_string.p90.json diff --git a/Performance/Benchmarks/Thresholds/main/GRPCSwiftBenchmark.Metadata_Iterate_all_values.p90.json b/IntegrationTests/Benchmarks/Thresholds/6.1/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/6.1/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/6.1/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/6.1/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_binary_values_stored.p90.json 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..9c403f41f --- /dev/null +++ b/IntegrationTests/Benchmarks/Thresholds/6.1/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_strings_stored.p90.json @@ -0,0 +1,3 @@ +{ + "mallocCountTotal": 1000 +} diff --git a/Performance/Benchmarks/Thresholds/main/GRPCSwiftBenchmark.Metadata_Iterate_string_values.p90.json b/IntegrationTests/Benchmarks/Thresholds/6.1/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/6.1/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/6.1/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/6.1/GRPCSwiftBenchmark.Metadata_Remove_values_for_key.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/nightly-main/GRPCSwiftBenchmark.Metadata_Add_binary.p90.json similarity index 55% rename from Performance/Benchmarks/Thresholds/6.0/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_strings_stored.p90.json rename to IntegrationTests/Benchmarks/Thresholds/nightly-main/GRPCSwiftBenchmark.Metadata_Add_binary.p90.json index b59f05063..b642696c1 100644 --- a/Performance/Benchmarks/Thresholds/6.0/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_strings_stored.p90.json +++ b/IntegrationTests/Benchmarks/Thresholds/nightly-main/GRPCSwiftBenchmark.Metadata_Add_binary.p90.json @@ -1,7 +1,7 @@ { - "mallocCountTotal" : 2000, + "mallocCountTotal" : 11, "memoryLeaked" : 0, - "releaseCount" : 6001, + "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..9c403f41f --- /dev/null +++ b/IntegrationTests/Benchmarks/Thresholds/nightly-main/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_strings_stored.p90.json @@ -0,0 +1,3 @@ +{ + "mallocCountTotal": 1000 +} 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/IntegrationTests/Benchmarks/Thresholds/nightly-next/GRPCSwiftBenchmark.Metadata_Add_binary.p90.json b/IntegrationTests/Benchmarks/Thresholds/nightly-next/GRPCSwiftBenchmark.Metadata_Add_binary.p90.json new file mode 100644 index 000000000..b642696c1 --- /dev/null +++ b/IntegrationTests/Benchmarks/Thresholds/nightly-next/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-next/GRPCSwiftBenchmark.Metadata_Add_string.p90.json b/IntegrationTests/Benchmarks/Thresholds/nightly-next/GRPCSwiftBenchmark.Metadata_Add_string.p90.json new file mode 100644 index 000000000..7fde30a69 --- /dev/null +++ b/IntegrationTests/Benchmarks/Thresholds/nightly-next/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-next/GRPCSwiftBenchmark.Metadata_Iterate_all_values.p90.json b/IntegrationTests/Benchmarks/Thresholds/nightly-next/GRPCSwiftBenchmark.Metadata_Iterate_all_values.p90.json new file mode 100644 index 000000000..1b1303873 --- /dev/null +++ b/IntegrationTests/Benchmarks/Thresholds/nightly-next/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-next/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 new file mode 100644 index 000000000..1b1303873 --- /dev/null +++ b/IntegrationTests/Benchmarks/Thresholds/nightly-next/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-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 new file mode 100644 index 000000000..9c403f41f --- /dev/null +++ b/IntegrationTests/Benchmarks/Thresholds/nightly-next/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_strings_stored.p90.json @@ -0,0 +1,3 @@ +{ + "mallocCountTotal": 1000 +} diff --git a/IntegrationTests/Benchmarks/Thresholds/nightly-next/GRPCSwiftBenchmark.Metadata_Iterate_string_values.p90.json b/IntegrationTests/Benchmarks/Thresholds/nightly-next/GRPCSwiftBenchmark.Metadata_Iterate_string_values.p90.json new file mode 100644 index 000000000..1b1303873 --- /dev/null +++ b/IntegrationTests/Benchmarks/Thresholds/nightly-next/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-next/GRPCSwiftBenchmark.Metadata_Remove_values_for_key.p90.json b/IntegrationTests/Benchmarks/Thresholds/nightly-next/GRPCSwiftBenchmark.Metadata_Remove_values_for_key.p90.json new file mode 100644 index 000000000..5750750bc --- /dev/null +++ b/IntegrationTests/Benchmarks/Thresholds/nightly-next/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/NOTICES.txt b/NOTICES.txt index 3a8509e83..61ce63d4e 100644 --- a/NOTICES.txt +++ b/NOTICES.txt @@ -17,23 +17,9 @@ 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'. - + * LICENSE (Apache License 2.0): * https://github.com/apple/swift-nio-http2/blob/main/LICENSE.txt * HOMEPAGE: @@ -41,31 +27,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..0b1839e24 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,117 @@ * 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 CompilerPluginSupport +import PackageDescription -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" +let products: [Product] = [ + .library( + name: "GRPCCore", + targets: ["GRPCCore"] ), - .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") +] + +// ------------------------------------------------------------------------------------------------- + +// 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) } -// 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" - ] - ) - - 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: "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" - ) +let defaultSwiftSettings: [SwiftSetting] = + availabilitySettings + [ + .swiftLanguageMode(.v6), + .enableUpcomingFeature("ExistentialAny"), + .enableUpcomingFeature("InternalImportsByDefault"), + .enableUpcomingFeature("MemberImportVisibility"), + ] - 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", +let targets: [Target] = [ + // Runtime serialization components + .target( + name: "GRPCCore", dependencies: [ - .grpc, - .helloWorldModel, - .nioCore, - .nioPosix, - .argumentParser, + .product(name: "DequeModule", package: "swift-collections") ], - 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", + swiftSettings: defaultSwiftSettings + ), + .testTarget( + name: "GRPCCoreTests", dependencies: [ - .grpc, - .routeGuideModel, - .nioCore, - .nioPosix, - .argumentParser, + .target(name: "GRPCCore"), + .target(name: "GRPCInProcessTransport"), + .product(name: "SwiftProtobuf", package: "swift-protobuf"), ], - path: "Examples/v1/RouteGuide/Client" - ) - - static let routeGuideServer: Target = .executableTarget( - name: "RouteGuideServer", - dependencies: [ - .grpc, - .routeGuideModel, - .nioCore, - .nioConcurrencyHelpers, - .nioPosix, - .argumentParser, + resources: [ + .copy("Configuration/Inputs") ], - path: "Examples/v1/RouteGuide/Server" - ) + swiftSettings: defaultSwiftSettings + ), - static let packetCapture: Target = .executableTarget( - name: "PacketCapture", + // In-process client and server transport implementations + .target( + name: "GRPCInProcessTransport", dependencies: [ - .grpc, - .echoModel, - .nioCore, - .nioPosix, - .nioExtras, - .argumentParser, + .target(name: "GRPCCore") ], - path: "Examples/v1/PacketCapture", - exclude: [ - "README.md", - ] - ) - - static let reflectionService: Target = .target( - name: "GRPCReflectionService", + swiftSettings: defaultSwiftSettings + ), + .testTarget( + name: "GRPCInProcessTransportTests", dependencies: [ - .grpc, - .nio, - .protobuf, + .target(name: "GRPCInProcessTransport") ], - path: "Sources/GRPCReflectionService" - ) + swiftSettings: defaultSwiftSettings + ), - static let reflectionServer: Target = .executableTarget( - name: "ReflectionServer", + // Code generator library for protoc-gen-grpc-swift + .target( + name: "GRPCCodeGen", + dependencies: [], + swiftSettings: defaultSwiftSettings + ), + .testTarget( + name: "GRPCCodeGenTests", dependencies: [ - .grpc, - .reflectionService, - .helloWorldModel, - .nioCore, - .nioPosix, - .argumentParser, - .echoModel, - .echoImplementation + .target(name: "GRPCCodeGen") ], - path: "Examples/v1/ReflectionService", - resources: [ - .copy("Generated") - ] - ) -} - -// 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 + swiftSettings: defaultSwiftSettings + ), +] 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/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 deleted file mode 100644 index b4aba1c3f..000000000 --- a/Performance/Benchmarks/Thresholds/main/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_strings_stored.p90.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "mallocCountTotal" : 2000, - "memoryLeaked" : 0, - "releaseCount" : 7001, - "retainCount" : 3000, - "syscalls" : 0 -} 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/fetch.sh b/Protos/fetch.sh deleted file mode 100755 index 5d0cee63b..000000000 --- a/Protos/fetch.sh +++ /dev/null @@ -1,55 +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 )" -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/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" -cp -rp "$checkouts/googleapis/google/rpc/code.proto" "$upstream/google/rpc/code.proto" 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/Protos/upstream/grpc/core/stats.proto b/Protos/upstream/grpc/core/stats.proto deleted file mode 100644 index ac181b043..000000000 --- a/Protos/upstream/grpc/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/Protos/upstream/grpc/health/v1/health.proto b/Protos/upstream/grpc/health/v1/health.proto deleted file mode 100644 index 13b03f567..000000000 --- a/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/Protos/upstream/grpc/reflection/v1/reflection.proto b/Protos/upstream/grpc/reflection/v1/reflection.proto deleted file mode 100644 index 1a2ceedc3..000000000 --- a/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/Protos/upstream/grpc/reflection/v1alpha/reflection.proto b/Protos/upstream/grpc/reflection/v1alpha/reflection.proto deleted file mode 100644 index 9cab8a4e0..000000000 --- a/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/Protos/upstream/grpc/testing/benchmark_service.proto b/Protos/upstream/grpc/testing/benchmark_service.proto deleted file mode 100644 index 5209bd6ef..000000000 --- a/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/Protos/upstream/grpc/testing/control.proto b/Protos/upstream/grpc/testing/control.proto deleted file mode 100644 index e309e5f9c..000000000 --- a/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/Protos/upstream/grpc/testing/messages.proto b/Protos/upstream/grpc/testing/messages.proto deleted file mode 100644 index 99e34dcc8..000000000 --- a/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/Protos/upstream/grpc/testing/payloads.proto b/Protos/upstream/grpc/testing/payloads.proto deleted file mode 100644 index 8cbc9db6c..000000000 --- a/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/Protos/upstream/grpc/testing/stats.proto b/Protos/upstream/grpc/testing/stats.proto deleted file mode 100644 index 1f0fae4e5..000000000 --- a/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/Protos/upstream/grpc/testing/worker_service.proto b/Protos/upstream/grpc/testing/worker_service.proto deleted file mode 100644 index ff3aa2931..000000000 --- a/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/README.md b/README.md index df7eced27..a6ea9c736 100644 --- a/README.md +++ b/README.md @@ -1,30 +1,52 @@ # 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]. + +- ๐Ÿ“š **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) +- ๐Ÿ”€ **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 + +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: "Application", + platforms: [.macOS("15.0")], + dependencies: [ + .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: "Server", + dependencies: [ + .product(name: "GRPCCore", package: "grpc-swift"), + .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 +[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 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/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/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/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 700f33866..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 @@ -44,11 +45,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,144 +59,129 @@ 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 } +} - /// 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 - } +@available(gRPCSwift 2.0, *) +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 + ) + } +} - /// 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 +/// 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. + public var item: Item? - /// The name of the imported item. - public var name: String + /// The access level to be included in imports of this dependency. + public var accessLevel: CodeGenerator.Config.AccessLevel - public init(kind: Kind, name: String) { - self.kind = kind - self.name = name - } + /// The name of the imported module or of the module an item is imported from. + public var module: 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`) - } - } + /// 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: CodeGenerator.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,134 +190,305 @@ 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 attribute is not required. - public static var notRequired: Self { - Self(.notRequired) + /// The imported item is a let. + public static var `let`: Self { + Self(.`let`) } - /// 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 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: 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) } - } - /// 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 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. +@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. + public var documentation: String + + /// 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 + } +} + +@available(gRPCSwift 2.0, *) +extension ServiceDescriptor { + @available(*, deprecated, renamed: "init(documentation:name:methods:)") + public init( + documentation: String, + name: Name, + namespace: Name, + methods: [MethodDescriptor] + ) { + self.documentation = documentation + 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 + ) + } +} + +/// 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. + 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: MethodName + + /// 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: MethodName, + 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 + } +} + +@available(gRPCSwift 2.0, *) +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 + } +} + +@available(gRPCSwift 2.0, *) +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 + } +} + +@available(gRPCSwift 2.0, *) +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.") +@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. + /// + /// 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 CodeGenerationRequest.Name { +@available(*, deprecated, message: "Use ServiceName/MethodName instead.") +@available(gRPCSwift 2.0, *) +extension 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: "_") + return self.base.replacing(".", with: "_") } } diff --git a/Sources/GRPCCodeGen/SourceGenerator.swift b/Sources/GRPCCodeGen/CodeGenerator.swift similarity index 55% rename from Sources/GRPCCodeGen/SourceGenerator.swift rename to Sources/GRPCCodeGen/CodeGenerator.swift index 454258bc6..56de6efc6 100644 --- a/Sources/GRPCCodeGen/SourceGenerator.swift +++ b/Sources/GRPCCodeGen/CodeGenerator.swift @@ -14,8 +14,14 @@ * 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") +@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. public var config: Config @@ -36,6 +42,12 @@ public struct SourceGenerator: Sendable { public var client: Bool /// 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. /// @@ -57,12 +69,13 @@ public struct SourceGenerator: Sendable { self.indentation = indentation self.client = client self.server = server + self.grpcCoreModuleName = "GRPCCore" } /// 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` @@ -77,10 +90,55 @@ public struct SourceGenerator: 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. + @available(gRPCSwift 2.2, *) + 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)) + } + } } - /// 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 { @@ -92,10 +150,27 @@ public struct SourceGenerator: 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, + availability: AvailabilityDescription(self.config.availability) ) + let sourceFile = try textRenderer.render(structured: structuredSwiftRepresentation) return sourceFile } } + +@available(gRPCSwift 2.0, *) +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/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/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 fe7f038a6..3cfb4a2dd 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. /// @@ -70,6 +69,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 @@ -109,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( @@ -610,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): @@ -731,10 +741,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 { @@ -1131,8 +1162,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. @@ -1176,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 new file mode 100644 index 000000000..c9c7d9545 --- /dev/null +++ b/Sources/GRPCCodeGen/Internal/StructuredSwift+Client.swift @@ -0,0 +1,838 @@ +/* + * Copyright 2024, gRPC Authors All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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, + namer: Namer = Namer() + ) -> 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: namer.clientRequest(forType: input, isStreaming: streamingInput) + ) + ) + + if includeSerializers { + signature.parameters.append( + ParameterDescription( + label: "serializer", + // Type is optional, so be explicit about which 'some' to use + 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(namer.deserializer(forType: output)) + ) + ) + } + + signature.parameters.append( + ParameterDescription( + label: "options", + type: namer.callOptions, + defaultValue: includeDefaults ? .memberAccess(.dot("defaults")) : nil + ) + ) + + signature.parameters.append( + ParameterDescription( + label: "onResponse", + name: "handleResponse", + type: .closure( + ClosureSignatureDescription( + parameters: [ + ParameterDescription( + type: namer.clientResponse(forType: output, isStreaming: 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, + namer: Namer = Namer() + ) -> Self { + FunctionDescription( + signature: .clientMethod( + accessLevel: accessLevel, + name: name, + input: input, + output: output, + streamingInput: streamingInput, + streamingOutput: streamingOutput, + includeDefaults: true, + includeSerializers: false, + namer: namer + ), + 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") + ), + ] + ) + ) + ) + ) + ] + ) + } +} + +@available(gRPCSwift 2.0, *) +extension ProtocolDescription { + /// ``` + /// protocol : Sendable { + /// func foo( + /// ... + /// ) async throws -> Result + /// } + /// ``` + static func clientProtocol( + accessLevel: AccessModifier? = nil, + name: String, + methods: [MethodDescriptor], + namer: Namer = Namer() + ) -> Self { + ProtocolDescription( + accessModifier: accessLevel, + name: name, + conformances: ["Sendable"], + members: methods.map { method in + .commentable( + .preFormatted(docs(for: method)), + .function( + signature: .clientMethod( + name: method.name.functionName, + input: method.inputType, + output: method.outputType, + streamingInput: method.isInputStreaming, + streamingOutput: method.isOutputStreaming, + includeDefaults: false, + includeSerializers: true, + namer: namer + ) + ) + ) + } + ) + } +} + +@available(gRPCSwift 2.0, *) +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], + namer: Namer = Namer(), + serializer: (String) -> String, + deserializer: (String) -> String + ) -> Self { + ExtensionDescription( + onType: name, + declarations: methods.map { method in + .commentable( + .preFormatted(docs(for: method, serializers: false)), + .function( + .clientMethodWithDefaults( + accessLevel: accessLevel, + name: method.name.functionName, + input: method.inputType, + output: method.outputType, + streamingInput: method.isInputStreaming, + streamingOutput: method.isOutputStreaming, + serializer: .identifierPattern(serializer(method.inputType)), + deserializer: .identifierPattern(deserializer(method.outputType)), + namer: namer + ) + ) + ) + } + ) + } +} + +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, + namer: Namer = Namer() + ) -> 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: namer.metadata, + defaultValue: .literal(.dictionary([])) + ) + ) + + // options: GRPCCore.CallOptions = .defaults + signature.parameters.append( + ParameterDescription( + label: "options", + type: namer.callOptions, + defaultValue: .dot("defaults") + ) + ) + + if streamingInput { + signature.parameters.append( + ParameterDescription( + label: "requestProducer", + name: "producer", + type: .closure( + ClosureSignatureDescription( + parameters: [ParameterDescription(type: namer.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: namer.clientResponse(forType: output, isStreaming: 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, + namer: Namer = Namer() + ) -> 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( + namer.clientRequest(forType: input, isStreaming: 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, + namer: Namer = Namer() + ) -> Self { + FunctionDescription( + signature: .clientMethodExploded( + accessLevel: accessLevel, + name: name, + input: input, + output: output, + streamingInput: streamingInput, + streamingOutput: streamingOutput, + namer: namer + ), + body: .clientMethodExploded( + name: name, + input: input, + streamingInput: streamingInput, + namer: namer + ) + ) + } +} + +@available(gRPCSwift 2.0, *) +extension ExtensionDescription { + /// ``` + /// extension { + /// // (exploded client methods) + /// } + /// ``` + static func explodedClientMethods( + accessLevel: AccessModifier? = nil, + on extensionName: String, + methods: [MethodDescriptor], + namer: Namer = Namer() + ) -> ExtensionDescription { + return ExtensionDescription( + onType: extensionName, + declarations: methods.map { method in + .commentable( + .preFormatted(explodedDocs(for: method)), + .function( + .clientMethodExploded( + accessLevel: accessLevel, + name: method.name.functionName, + input: method.inputType, + output: method.outputType, + streamingInput: method.isInputStreaming, + streamingOutput: method.isOutputStreaming, + namer: namer + ) + ) + ) + } + ) + } +} + +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, + namer: Namer = Namer() + ) -> 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: 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(namer.serializer(forType: input)) + ), + ParameterDescription( + label: "deserializer", + // Be explicit: 'type' is optional and '.some' resolves to Optional.some by default. + type: ExistingTypeDescription.some(namer.deserializer(forType: output)) + ), + ParameterDescription( + label: "options", + type: namer.callOptions, + defaultValue: .dot("defaults") + ), + ParameterDescription( + label: "onResponse", + name: "handleResponse", + type: .closure( + ClosureSignatureDescription( + parameters: [ + ParameterDescription( + type: namer.clientResponse(forType: output, isStreaming: 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") + ), + ] + ) + ) + ) + ] + ) + } +} + +@available(gRPCSwift 2.0, *) +extension StructDescription { + /// ``` + /// struct : where Transport: GRPCCore.ClientTransport { + /// 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], + namer: Namer = Namer() + ) -> Self { + return StructDescription( + accessModifier: accessLevel, + name: name, + generics: [.member("Transport")], + conformances: [clientProtocol], + whereClause: WhereClause( + requirements: [.conformance("Transport", namer.literalNamespacedType("ClientTransport"))] + ), + members: [ + .variable( + accessModifier: .private, + kind: .let, + left: "client", + type: namer.grpcClient(genericOver: "Transport") + ), + .commentable( + .preFormatted( + """ + /// Creates a new client wrapping the provided `\(namer.literalNamespacedType("GRPCClient"))`. + /// + /// - Parameters: + /// - client: A `\(namer.literalNamespacedType("GRPCClient"))` providing a communication channel to the service. + """ + ), + .function( + accessModifier: accessLevel, + kind: .initializer, + parameters: [ + ParameterDescription( + label: "wrapping", + name: "client", + type: namer.grpcClient( + genericOver: "Transport" + ) + ) + ], + whereClause: nil, + body: [ + .expression( + .assignment( + left: .identifierPattern("self").dot("client"), + right: .identifierPattern("client") + ) + ) + ] + ) + ), + ] + + methods.map { method in + .commentable( + .preFormatted(docs(for: method)), + .function( + .clientMethod( + accessLevel: accessLevel, + name: method.name.functionName, + input: method.inputType, + output: method.outputType, + serviceEnum: serviceEnum, + methodEnum: method.name.typeName, + streamingInput: method.isInputStreaming, + streamingOutput: method.isOutputStreaming, + namer: namer + ) + ) + ) + } + ) + } +} + +@available(gRPCSwift 2.0, *) +private func docs( + for method: MethodDescriptor, + serializers includeSerializers: Bool = true +) -> String { + let summary = "/// Call the \"\(method.name.identifyingName)\" 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) +} + +@available(gRPCSwift 2.0, *) +private func explodedDocs(for method: MethodDescriptor) -> String { + let summary = "/// Call the \"\(method.name.identifyingName)\" 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 new file mode 100644 index 000000000..5cd883caa --- /dev/null +++ b/Sources/GRPCCodeGen/Internal/StructuredSwift+Server.swift @@ -0,0 +1,802 @@ +/* + * Copyright 2024, gRPC Authors All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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, + namer: Namer = Namer() + ) -> Self { + return FunctionSignatureDescription( + accessModifier: accessLevel, + kind: .function(name: name), + parameters: [ + ParameterDescription( + label: "request", + type: namer.serverRequest(forType: input, isStreaming: streamingInput) + ), + ParameterDescription(label: "context", type: namer.serverContext), + ], + keywords: [.async, .throws], + returnType: .identifierType( + namer.serverResponse(forType: output, isStreaming: streamingOutput) + ) + ) + } +} + +@available(gRPCSwift 2.0, *) +extension ProtocolDescription { + /// ``` + /// protocol : GRPCCore.RegistrableRPCService { + /// ... + /// } + /// ``` + static func streamingService( + accessLevel: AccessModifier? = nil, + name: String, + methods: [MethodDescriptor], + namer: Namer = Namer() + ) -> Self { + func docs(for method: MethodDescriptor) -> String { + let summary = """ + /// Handle the "\(method.name.identifyingName)" 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: [namer.literalNamespacedType("RegistrableRPCService")], + members: methods.map { method in + .commentable( + .preFormatted(docs(for: method)), + .function( + signature: .serverMethod( + name: method.name.functionName, + input: method.inputType, + output: method.outputType, + streamingInput: true, + streamingOutput: true, + namer: namer + ) + ) + ) + } + ) + } +} + +@available(gRPCSwift 2.0, *) +extension ExtensionDescription { + /// ``` + /// extension { + /// func registerMethods(with router: inout GRPCCore.RPCRouter) { + /// // ... + /// } + /// } + /// ``` + static func registrableRPCServiceDefaultImplementation( + accessLevel: AccessModifier? = nil, + on extensionName: String, + serviceNamespace: String, + methods: [MethodDescriptor], + namer: Namer = Namer(), + serializer: (String) -> String, + deserializer: (String) -> String + ) -> Self { + return ExtensionDescription( + onType: extensionName, + declarations: [ + .function( + .registerMethods( + accessLevel: accessLevel, + serviceNamespace: serviceNamespace, + methods: methods, + namer: namer, + serializer: serializer, + deserializer: deserializer + ) + ) + ] + ) + } +} + +@available(gRPCSwift 2.0, *) +extension ProtocolDescription { + /// ``` + /// protocol : { + /// ... + /// } + /// ``` + static func service( + accessLevel: AccessModifier? = nil, + name: String, + streamingProtocol: String, + methods: [MethodDescriptor], + namer: Namer = Namer() + ) -> Self { + func docs(for method: MethodDescriptor) -> String { + let summary = """ + /// Handle the "\(method.name.identifyingName)" 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(docs(for: method)), + .function( + signature: .serverMethod( + name: method.name.functionName, + input: method.inputType, + output: method.outputType, + streamingInput: method.isInputStreaming, + streamingOutput: method.isOutputStreaming, + namer: namer + ) + ) + ) + } + ) + } +} + +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)) + ), + ] + ) + } +} + +@available(gRPCSwift 2.0, *) +extension FunctionDescription { + /// ``` + /// func registerMethods(with router: inout GRPCCore.RPCRouter) { + /// // ... + /// } + /// ``` + static func registerMethods( + accessLevel: AccessModifier? = nil, + serviceNamespace: String, + methods: [MethodDescriptor], + namer: Namer = Namer(), + serializer: (String) -> String, + deserializer: (String) -> String + ) -> Self { + return FunctionDescription( + accessModifier: accessLevel, + kind: .function(name: "registerMethods"), + generics: [.member("Transport")], + parameters: [ + ParameterDescription( + label: "with", + name: "router", + type: namer.rpcRouter(genericOver: "Transport"), + `inout`: true + ) + ], + whereClause: WhereClause( + requirements: [ + .conformance("Transport", namer.literalNamespacedType("ServerTransport")) + ] + ), + body: methods.map { method in + .functionCall( + .registerWithRouter( + serviceNamespace: serviceNamespace, + methodNamespace: method.name.typeName, + methodName: method.name.functionName, + 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, + namer: Namer = Namer() + ) -> 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, + namer: namer + ) + + // 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( + namer.serverRequest(forType: nil, isStreaming: 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(namer.serverResponse(forType: nil, isStreaming: true)), + arguments: [ + FunctionArgumentDescription( + label: "single", + expression: .identifierPattern("response") + ) + ] + ) + ) + + return Self( + signature: signature, + body: [.declaration(response), .expression(returnExpression)] + ) + } +} + +@available(gRPCSwift 2.0, *) +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], + namer: Namer = Namer() + ) -> 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.functionName, + input: method.inputType, + output: method.outputType, + streamingInput: method.isInputStreaming, + streamingOutput: method.isOutputStreaming, + namer: namer + ) + ) + } + ) + } +} + +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, + namer: Namer = Namer() + ) -> Self { + var parameters: [ParameterDescription] = [ + ParameterDescription( + label: "request", + type: streamingInput ? namer.rpcAsyncSequence(forType: input) : .member(input) + ) + ] + + if streamingOutput { + parameters.append( + ParameterDescription( + label: "response", + type: namer.rpcWriter(forType: output) + ) + ) + } + + parameters.append(ParameterDescription(label: "context", type: namer.serverContext)) + + return FunctionSignatureDescription( + accessModifier: accessLevel, + kind: .function(name: name), + parameters: parameters, + keywords: [.async, .throws], + returnType: streamingOutput ? nil : .identifier(.pattern(output)) + ) + } +} + +@available(gRPCSwift 2.0, *) +extension ProtocolDescription { + /// ``` + /// protocol SimpleServiceProtocol: { + /// ... + /// } + /// ``` + static func simpleServiceProtocol( + accessModifier: AccessModifier? = nil, + name: String, + serviceProtocol: String, + methods: [MethodDescriptor], + namer: Namer = Namer() + ) -> Self { + func docs(for method: MethodDescriptor) -> String { + let summary = """ + /// Handle the "\(method.name.identifyingName)" 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.functionName, + input: method.inputType, + output: method.outputType, + streamingInput: method.isInputStreaming, + streamingOutput: method.isOutputStreaming, + namer: namer + ) + ) + ) + } + ) + } +} + +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, + namer: Namer = Namer() + ) -> 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, + namer: namer + ), + body: [ + .expression( + .functionCall( + calledExpression: .return( + .identifierType( + namer.serverResponse(forType: output, isStreaming: streamingOutput) + ) + ), + arguments: streamingOutput ? makeStreamingOutputArguments() : makeUnaryOutputArguments() + ) + ) + ] + ) + } +} + +@available(gRPCSwift 2.0, *) +extension ExtensionDescription { + /// ``` + /// extension ServiceProtocol { + /// ... + /// } + /// ``` + static func serviceProtocolDefaultImplementation( + accessModifier: AccessModifier? = nil, + on extensionName: String, + methods: [MethodDescriptor], + namer: Namer = Namer() + ) -> Self { + ExtensionDescription( + onType: extensionName, + declarations: methods.map { method in + .function( + .serviceProtocolDefaultImplementation( + accessModifier: accessModifier, + name: method.name.functionName, + input: method.inputType, + output: method.outputType, + streamingInput: method.isInputStreaming, + streamingOutput: method.isOutputStreaming, + namer: namer + ) + ) + } + ) + } +} diff --git a/Sources/GRPCCodeGen/Internal/StructuredSwift+ServiceMetadata.swift b/Sources/GRPCCodeGen/Internal/StructuredSwift+ServiceMetadata.swift new file mode 100644 index 000000000..8ed3cc885 --- /dev/null +++ b/Sources/GRPCCodeGen/Internal/StructuredSwift+ServiceMetadata.swift @@ -0,0 +1,391 @@ +/* + * Copyright 2024, gRPC Authors All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 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( + 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: GRPCCore.ServiceDescriptor(fullyQualifiedServiceName: ""), + /// method: "" + /// ) + /// ``` + package static func methodDescriptor( + accessModifier: AccessModifier? = nil, + literalFullyQualifiedService: String, + literalMethodName: String, + namer: Namer = Namer() + ) -> Self { + return VariableDescription( + accessModifier: accessModifier, + isStatic: true, + kind: .let, + left: .identifier(.pattern("descriptor")), + right: .functionCall( + FunctionCallDescription( + calledExpression: .identifierType(namer.methodDescriptor), + arguments: [ + FunctionArgumentDescription( + label: "service", + expression: .functionCall( + .serviceDescriptor( + literalFullyQualifiedService: literalFullyQualifiedService, + namer: namer + ) + ) + ), + FunctionArgumentDescription( + label: "method", + expression: .literal(literalMethodName) + ), + ] + ) + ) + ) + } + + /// ``` + /// static let descriptor = GRPCCore.ServiceDescriptor(fullyQualifiedService: ) + /// ``` + package static func serviceDescriptor( + accessModifier: AccessModifier? = nil, + literalFullyQualifiedService name: String, + namer: Namer = Namer() + ) -> Self { + return VariableDescription( + accessModifier: accessModifier, + isStatic: true, + kind: .let, + left: .identifierPattern("descriptor"), + right: .functionCall(.serviceDescriptor(literalFullyQualifiedService: name, namer: namer)) + ) + } +} + +extension FunctionCallDescription { + package static func serviceDescriptor( + literalFullyQualifiedService: String, + namer: Namer = Namer() + ) -> Self { + FunctionCallDescription( + calledExpression: .identifier(.type(namer.serviceDescriptor)), + arguments: [ + FunctionArgumentDescription( + label: "fullyQualifiedService", + expression: .literal(literalFullyQualifiedService) + ) + ] + ) + } +} + +extension ExtensionDescription { + /// ``` + /// extension GRPCCore.ServiceDescriptor { + /// static let = Self( + /// fullyQualifiedService: + /// ) + /// } + /// ``` + package static func serviceDescriptor( + accessModifier: AccessModifier? = nil, + propertyName: String, + literalFullyQualifiedService: String, + namer: Namer = Namer() + ) -> ExtensionDescription { + return ExtensionDescription( + onType: namer.literalNamespacedType("ServiceDescriptor"), + declarations: [ + .commentable( + .doc("Service descriptor for the \"\(literalFullyQualifiedService)\" service."), + .variable( + accessModifier: accessModifier, + isStatic: true, + kind: .let, + left: .identifier(.pattern(propertyName)), + right: .functionCall( + .serviceDescriptor( + literalFullyQualifiedService: literalFullyQualifiedService, + namer: namer + ) + ) + ) + ) + ] + ) + } +} + +extension VariableDescription { + /// ``` + /// static let descriptors: [GRPCCore.MethodDescriptor] = [.descriptor, ...] + /// ``` + package static func methodDescriptorsArray( + accessModifier: AccessModifier? = nil, + methodNamespaceNames names: [String], + namer: Namer = Namer() + ) -> Self { + return VariableDescription( + accessModifier: accessModifier, + isStatic: true, + kind: .let, + left: .identifier(.pattern("descriptors")), + type: .array(namer.methodDescriptor), + right: .literal(.array(names.map { name in .identifierPattern(name).dot("descriptor") })) + ) + } +} + +@available(gRPCSwift 2.0, *) +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, + literalFullyQualifiedService: String, + inputType: String, + outputType: String, + namer: Namer = Namer() + ) -> Self { + return EnumDescription( + accessModifier: accessModifier, + name: name, + members: [ + .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, + namer: namer + ) + ) + ), + ] + ) + } + + /// ``` + /// 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, + literalFullyQualifiedService: String, + methods: [MethodDescriptor], + namer: Namer = Namer() + ) -> EnumDescription { + var description = EnumDescription(accessModifier: accessModifier, name: "Method") + + // Add a namespace for each method. + let methodNamespaces: [Declaration] = methods.map { method in + return .commentable( + .doc("Namespace for \"\(method.name.typeName)\" metadata."), + .enum( + .methodNamespace( + accessModifier: accessModifier, + name: method.name.typeName, + literalMethod: method.name.identifyingName, + literalFullyQualifiedService: literalFullyQualifiedService, + inputType: method.inputType, + outputType: method.outputType, + namer: namer + ) + ) + ) + } + description.members.append(contentsOf: methodNamespaces) + + // Add an array of method descriptors + let methodDescriptorsArray: VariableDescription = .methodDescriptorsArray( + accessModifier: accessModifier, + methodNamespaceNames: methods.map { $0.name.typeName }, + namer: namer + ) + description.members.append( + .commentable( + .doc("Descriptors for all methods in the \"\(literalFullyQualifiedService)\" service."), + .variable(methodDescriptorsArray) + ) + ) + + return description + } + + /// ``` + /// enum { + /// static let descriptor = GRPCCore.ServiceDescriptor. + /// enum Method { + /// ... + /// } + /// } + /// ``` + package static func serviceNamespace( + accessModifier: AccessModifier? = nil, + name: String, + literalFullyQualifiedService: String, + 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, + namer: namer + ) + description.members.append( + .commentable( + .doc("Service descriptor for the \"\(literalFullyQualifiedService)\" service."), + .variable(descriptor) + ) + ) + + // enum Method { ... } + let methodsNamespace: EnumDescription = .methodsNamespace( + accessModifier: accessModifier, + literalFullyQualifiedService: literalFullyQualifiedService, + methods: methods, + namer: namer + ) + description.members.append( + .commentable( + .doc("Namespace for method metadata."), + .enum(methodsNamespace) + ) + ) + + return description + } +} + +@available(gRPCSwift 2.0, *) +extension [CodeBlock] { + /// ``` + /// enum { + /// ... + /// } + /// + /// extension GRPCCore.ServiceDescriptor { + /// ... + /// } + /// ``` + package static func serviceMetadata( + accessModifier: AccessModifier? = nil, + service: ServiceDescriptor, + availability: AvailabilityDescription, + namer: Namer = Namer() + ) -> Self { + var blocks: [CodeBlock] = [] + + let serviceNamespace: EnumDescription = .serviceNamespace( + accessModifier: accessModifier, + name: service.name.typeName, + literalFullyQualifiedService: service.name.identifyingName, + methods: service.methods, + namer: namer + ) + blocks.append( + CodeBlock( + comment: .doc( + "Namespace containing generated types for the \"\(service.name.identifyingName)\" service." + ), + item: .declaration(.guarded(availability, .enum(serviceNamespace))) + ) + ) + + let descriptorExtension: ExtensionDescription = .serviceDescriptor( + accessModifier: accessModifier, + propertyName: service.name.propertyName, + literalFullyQualifiedService: service.name.identifyingName, + namer: namer + ) + 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 c6171f72f..52ad2ffae 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 { +package 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` @@ -100,7 +100,7 @@ struct ImportDescription: Equatable, Codable { /// A description of an access modifier. /// /// For example: `public`. -internal enum AccessModifier: String, Sendable, Equatable, Codable { +package enum AccessModifier: String, Sendable, Equatable, Codable, CaseIterable { /// A declaration accessible outside of the module. case `public` @@ -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 @@ -401,11 +401,17 @@ struct StructDescription: Equatable, Codable { /// 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] = [] } @@ -413,7 +419,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 +447,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. /// @@ -453,10 +459,10 @@ indirect enum ExistingTypeDescription: Equatable, Codable { /// 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,12 +489,22 @@ indirect enum ExistingTypeDescription: Equatable, Codable { /// /// 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. /// /// For example `typealias Foo = Int`. -struct TypealiasDescription: Equatable, Codable { +struct TypealiasDescription: Equatable, Codable, Sendable { /// An access modifier. var accessModifier: AccessModifier? @@ -507,7 +523,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 +547,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 +577,7 @@ struct ParameterDescription: Equatable, Codable { } /// A function kind: `func` or `init`. -enum FunctionKind: Equatable, Codable { +enum FunctionKind: Equatable, Codable, Sendable { /// An initializer. /// @@ -578,7 +594,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 +609,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 +636,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 +719,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 +740,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 +756,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 +777,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 +789,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 +828,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,18 +840,18 @@ 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 { +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 { - 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 } @@ -843,25 +859,25 @@ struct AvailabilityDescription: Equatable, Codable { /// One of the possible OSes. // swift-format-ignore: DontRepeatTypeInStaticProperties - struct OS: Equatable, Codable { - 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") } } /// 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 +891,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 +910,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 +925,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 +940,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 +957,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 +974,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 +994,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 +1006,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 +1025,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 +1041,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 +1059,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 +1077,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 +1099,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 +1110,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 +1121,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 +1130,7 @@ struct TupleDescription: Equatable, Codable { } /// A Swift expression. -indirect enum Expression: Equatable, Codable { +indirect enum Expression: Equatable, Codable, Sendable { /// A literal. /// @@ -1191,7 +1207,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,17 +1217,17 @@ 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? /// The code block item that appears below the comment. - var item: CodeBlockItem + var item: CodeBlockItem? } /// 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 +1241,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 +1253,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 diff --git a/Sources/GRPCCodeGen/Internal/Translator/ClientCodeTranslator.swift b/Sources/GRPCCodeGen/Internal/Translator/ClientCodeTranslator.swift index 5ead61a67..1f38bea37 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 { @@ -75,623 +75,114 @@ /// } /// } ///``` -struct ClientCodeTranslator: SpecializedTranslator { - var accessLevel: SourceGenerator.Config.AccessLevel - - init(accessLevel: SourceGenerator.Config.AccessLevel) { - self.accessLevel = accessLevel - } - - func translate(from request: CodeGenerationRequest) throws -> [CodeBlock] { +@available(gRPCSwift 2.0, *) +struct ClientCodeTranslator { + init() {} + + func translate( + accessModifier: AccessModifier, + service: ServiceDescriptor, + availability: AvailabilityDescription, + namer: Namer = Namer(), + serializer: (String) -> String, + deserializer: (String) -> String + ) -> [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: CodeGenerationRequest.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: CodeGenerationRequest.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: 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 + let `extension` = ExtensionDescription( + onType: service.name.typeName, + declarations: [ + // protocol ClientProtocol { ... } + .commentable( + .preFormatted( + Docs.suffix( + self.clientProtocolDocs(serviceName: service.name.identifyingName), + withDocs: service.documentation ) - ) - ) - ) - } - - // 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) + ), + .protocol( + .clientProtocol( + accessLevel: accessModifier, + name: "ClientProtocol", + methods: service.methods, + namer: namer ) ) - ], - 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, - 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) - ) - } - } - - private func makeClientProtocolMethodCall( - 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.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) + + // struct Client: ClientProtocol { ... } + .commentable( + .preFormatted( + Docs.suffix( + self.clientDocs( + serviceName: service.name.identifyingName, + moduleName: namer.grpcCore + ), + withDocs: service.documentation + ) + ), + .struct( + .client( + accessLevel: accessModifier, + name: "Client", + serviceEnum: service.name.typeName, + clientProtocol: "ClientProtocol", + methods: service.methods, + namer: namer + ) ) ), - 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))] - } - - private func makeParameters( - for method: CodeGenerationRequest.ServiceDescriptor.MethodDescriptor, - in service: CodeGenerationRequest.ServiceDescriptor, - from codeGenerationRequest: CodeGenerationRequest, - includeSerializationParameters: Bool, - includeDefaultCallOptions: Bool, - includeDefaultResponseHandler: Bool - ) -> [ParameterDescription] { - var parameters = [ParameterDescription]() - - 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: CodeGenerationRequest.ServiceDescriptor.MethodDescriptor, - in service: CodeGenerationRequest.ServiceDescriptor - ) -> ParameterDescription { - let requestType = method.isInputStreaming ? "Stream" : "Single" - let clientRequestType = ExistingTypeDescription.member([ - "GRPCCore", "ClientRequest", requestType, - ]) - return ParameterDescription( - label: "request", - type: .generic( - wrapper: clientRequestType, - wrapped: .member(method.inputType) - ) - ) - } + blocks.append(.declaration(.guarded(availability, .extension(`extension`)))) - private func serializerParameter( - for method: CodeGenerationRequest.ServiceDescriptor.MethodDescriptor, - in service: CodeGenerationRequest.ServiceDescriptor - ) -> ParameterDescription { - return ParameterDescription( - label: "serializer", - type: ExistingTypeDescription.some( - .generic( - wrapper: .member(["GRPCCore", "MessageSerializer"]), - wrapped: .member(method.inputType) - ) - ) + let extensionWithDefaults: ExtensionDescription = .clientMethodSignatureWithDefaults( + accessLevel: accessModifier, + name: "\(service.name.typeName).ClientProtocol", + methods: service.methods, + namer: namer, + serializer: serializer, + deserializer: deserializer ) - } - - private func deserializerParameter( - for method: CodeGenerationRequest.ServiceDescriptor.MethodDescriptor, - in service: CodeGenerationRequest.ServiceDescriptor - ) -> ParameterDescription { - return ParameterDescription( - label: "deserializer", - type: ExistingTypeDescription.some( - .generic( - wrapper: .member(["GRPCCore", "MessageDeserializer"]), - wrapped: .member(method.outputType) - ) + blocks.append( + CodeBlock( + comment: .inline("Helpers providing default arguments to 'ClientProtocol' methods."), + item: .declaration(.guarded(availability, .extension(extensionWithDefaults))) ) ) - } - - private func bodyParameter( - for method: CodeGenerationRequest.ServiceDescriptor.MethodDescriptor, - in service: CodeGenerationRequest.ServiceDescriptor, - includeDefaultResponseHandler: Bool - ) -> ParameterDescription { - let clientStreaming = method.isOutputStreaming ? "Stream" : "Single" - let closureParameterType = ExistingTypeDescription.generic( - wrapper: .member(["GRPCCore", "ClientResponse", clientStreaming]), - wrapped: .member(method.outputType) - ) - 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: "\(service.name.typeName).ClientProtocol", + methods: service.methods, + namer: namer ) - - var defaultResponseHandler: Expression? = nil - - if includeDefaultResponseHandler { - defaultResponseHandler = .closureInvocation( - ClosureInvocationDescription( - body: [.expression(.try(.identifierPattern("$0").dot("message")))] - ) + blocks.append( + CodeBlock( + comment: .inline("Helpers providing sugared APIs for 'ClientProtocol' methods."), + item: .declaration(.guarded(availability, .extension(extensionWithExplodedAPI))) ) - } - - return ParameterDescription( - name: "body", - type: .closure(bodyClosure), - defaultValue: defaultResponseHandler - ) - } - - private func makeClientStruct( - for service: CodeGenerationRequest.ServiceDescriptor, - in codeGenerationRequest: CodeGenerationRequest - ) -> Declaration { - let clientProperty = Declaration.variable( - accessModifier: .private, - kind: .let, - left: "client", - type: .member(["GRPCCore", "GRPCClient"]) ) - let initializer = self.makeClientVariable() - let methods = service.methods.map { - Declaration.commentable( - .preFormatted($0.documentation), - self.makeClientMethod(for: $0, in: service, from: codeGenerationRequest) - ) - } - 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" - } + return blocks } - 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, - 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))] - ) + 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``. + """ } - fileprivate enum InputOutputType { - case input - case output + 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 `\(moduleName).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..2e6505406 --- /dev/null +++ b/Sources/GRPCCodeGen/Internal/Translator/Docs.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. + */ + +@available(gRPCSwift 2.0, *) +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 + 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/Translator/IDLToStructuredSwiftTranslator.swift b/Sources/GRPCCodeGen/Internal/Translator/IDLToStructuredSwiftTranslator.swift index e0899291f..038254cf3 100644 --- a/Sources/GRPCCodeGen/Internal/Translator/IDLToStructuredSwiftTranslator.swift +++ b/Sources/GRPCCodeGen/Internal/Translator/IDLToStructuredSwiftTranslator.swift @@ -17,42 +17,92 @@ /// 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 { +@available(gRPCSwift 2.0, *) +package struct IDLToStructuredSwiftTranslator { + package init() {} + func translate( codeGenerationRequest: CodeGenerationRequest, - accessLevel: SourceGenerator.Config.AccessLevel, + accessLevel: CodeGenerator.Config.AccessLevel, accessLevelOnImports: Bool, client: Bool, - server: Bool + server: Bool, + grpcCoreModuleName: String, + availability: AvailabilityDescription ) throws -> StructuredSwiftRepresentation { try self.validateInput(codeGenerationRequest) + let accessModifier = AccessModifier(accessLevel) - var codeBlocks = [CodeBlock]() + var codeBlocks: [CodeBlock] = [] + let metadataTranslator = MetadataTranslator() + let serverTranslator = ServerCodeTranslator() + let clientTranslator = ClientCodeTranslator() - let typealiasTranslator = TypealiasTranslator( - client: client, - server: server, - accessLevel: accessLevel - ) - codeBlocks.append(contentsOf: try typealiasTranslator.translate(from: codeGenerationRequest)) + let namer = Namer(grpcCore: grpcCoreModuleName) + + for service in codeGenerationRequest.services { + codeBlocks.append( + CodeBlock(comment: .mark("\(service.name.identifyingName)", sectionBreak: true)) + ) + + let metadata = metadataTranslator.translate( + accessModifier: accessModifier, + service: service, + availability: availability, + namer: namer + ) + codeBlocks.append(contentsOf: metadata) + + if server { + codeBlocks.append( + CodeBlock(comment: .mark("\(service.name.identifyingName) (server)", sectionBreak: false)) + ) - if server { - let serverCodeTranslator = ServerCodeTranslator(accessLevel: accessLevel) - codeBlocks.append(contentsOf: try serverCodeTranslator.translate(from: codeGenerationRequest)) + let blocks = serverTranslator.translate( + accessModifier: accessModifier, + service: service, + availability: availability, + namer: namer, + serializer: codeGenerationRequest.makeSerializerCodeSnippet, + deserializer: codeGenerationRequest.makeDeserializerCodeSnippet + ) + codeBlocks.append(contentsOf: blocks) + } + + if client { + codeBlocks.append( + CodeBlock(comment: .mark("\(service.name.identifyingName) (client)", sectionBreak: false)) + ) + let blocks = clientTranslator.translate( + accessModifier: accessModifier, + service: service, + availability: availability, + namer: namer, + serializer: codeGenerationRequest.makeSerializerCodeSnippet, + deserializer: codeGenerationRequest.makeDeserializerCodeSnippet + ) + codeBlocks.append(contentsOf: blocks) + } } - if client { - let clientCodeTranslator = ClientCodeTranslator(accessLevel: accessLevel) - codeBlocks.append(contentsOf: try clientCodeTranslator.translate(from: codeGenerationRequest)) + let imports: [ImportDescription]? + if codeGenerationRequest.services.isEmpty { + imports = nil + codeBlocks.append( + CodeBlock(comment: .inline("This file contained no services.")) + ) + } else { + imports = try self.makeImports( + dependencies: codeGenerationRequest.dependencies, + accessLevel: accessLevel, + accessLevelOnImports: accessLevelOnImports, + grpcCoreModuleName: grpcCoreModuleName + ) } let fileDescription = FileDescription( topComment: .preFormatted(codeGenerationRequest.leadingTrivia), - imports: try self.makeImports( - dependencies: codeGenerationRequest.dependencies, - accessLevel: accessLevel, - accessLevelOnImports: accessLevelOnImports - ), + imports: imports, codeBlocks: codeBlocks ) @@ -61,16 +111,17 @@ struct IDLToStructuredSwiftTranslator: Translator { return StructuredSwiftRepresentation(file: file) } - private func makeImports( - dependencies: [CodeGenerationRequest.Dependency], - accessLevel: SourceGenerator.Config.AccessLevel, - accessLevelOnImports: Bool + package func makeImports( + dependencies: [Dependency], + accessLevel: CodeGenerator.Config.AccessLevel, + accessLevelOnImports: Bool, + grpcCoreModuleName: String ) throws -> [ImportDescription] { var imports: [ImportDescription] = [] imports.append( ImportDescription( accessLevel: accessLevelOnImports ? AccessModifier(accessLevel) : nil, - moduleName: "GRPCCore" + moduleName: grpcCoreModuleName ) ) @@ -86,8 +137,9 @@ struct IDLToStructuredSwiftTranslator: Translator { } } +@available(gRPCSwift 2.0, *) extension AccessModifier { - fileprivate init(_ accessLevel: SourceGenerator.Config.AccessLevel) { + init(_ accessLevel: CodeGenerator.Config.AccessLevel) { switch accessLevel.level { case .internal: self = .internal case .package: self = .package @@ -96,9 +148,10 @@ extension AccessModifier { } } +@available(gRPCSwift 2.0, *) extension IDLToStructuredSwiftTranslator { private func translateImport( - dependency: CodeGenerationRequest.Dependency, + dependency: Dependency, accessLevelOnImports: Bool ) throws -> ImportDescription { var importDescription = ImportDescription( @@ -135,7 +188,7 @@ extension IDLToStructuredSwiftTranslator { let servicesByGeneratedEnumName = Dictionary( grouping: codeGenerationRequest.services, - by: { $0.namespacedGeneratedName } + by: { $0.name.typeName } ) try self.checkServiceEnumNamesAreUnique(for: servicesByGeneratedEnumName) @@ -146,7 +199,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,56 +216,54 @@ 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. - 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. """ ) } } private func checkServiceDescriptorsAreUnique( - _ services: [CodeGenerationRequest.ServiceDescriptor] + _ services: [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 name = service.name.identifyingName let (inserted, _) = descriptors.insert(name) if !inserted { throw CodeGenError( @@ -227,24 +278,6 @@ extension IDLToStructuredSwiftTranslator { } } -extension CodeGenerationRequest.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/Tests/GRPCHTTP2TransportTests/Test Utilities/XCTest+Utilities.swift b/Sources/GRPCCodeGen/Internal/Translator/MetadataTranslator.swift similarity index 61% rename from Tests/GRPCHTTP2TransportTests/Test Utilities/XCTest+Utilities.swift rename to Sources/GRPCCodeGen/Internal/Translator/MetadataTranslator.swift index 3848388bc..6015a1057 100644 --- a/Tests/GRPCHTTP2TransportTests/Test Utilities/XCTest+Utilities.swift +++ b/Sources/GRPCCodeGen/Internal/Translator/MetadataTranslator.swift @@ -14,17 +14,21 @@ * limitations under the License. */ -import XCTest +@available(gRPCSwift 2.0, *) +struct MetadataTranslator { + init() {} -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 translate( + accessModifier: AccessModifier, + service: ServiceDescriptor, + availability: AvailabilityDescription, + namer: Namer = Namer() + ) -> [CodeBlock] { + .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 c264eea30..3de95fdc1 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,446 +43,181 @@ /// @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) /// } /// } ///``` -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))`." +@available(gRPCSwift 2.0, *) +struct ServerCodeTranslator { + init() {} + + func translate( + accessModifier: AccessModifier, + service: ServiceDescriptor, + availability: AvailabilityDescription, + namer: Namer = Namer(), + serializer: (String) -> String, + deserializer: (String) -> String + ) -> [CodeBlock] { + var blocks = [CodeBlock]() + + let `extension` = ExtensionDescription( + onType: service.name.typeName, + declarations: [ + // protocol StreamingServiceProtocol { ... } + .commentable( + .preFormatted( + Docs.suffix( + self.streamingServiceDocs(serviceName: service.name.identifyingName), + withDocs: service.documentation + ) ), - item: extensionServiceProtocol - ) - ) - } - - return codeBlocks - } -} - -extension ServerCodeTranslator { - private func makeStreamingProtocol( - for service: CodeGenerationRequest.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: CodeGenerationRequest.ServiceDescriptor.MethodDescriptor, - in service: CodeGenerationRequest.ServiceDescriptor, - accessModifier: AccessModifier? = nil - ) -> FunctionSignatureDescription { - return FunctionSignatureDescription( - accessModifier: accessModifier, - kind: .function(name: method.name.generatedLowerCase), - parameters: [ - .init( - label: "request", - type: .generic( - wrapper: .member(["GRPCCore", "ServerRequest", "Stream"]), - wrapped: .member(method.inputType) + .protocol( + .streamingService( + accessLevel: accessModifier, + name: "StreamingServiceProtocol", + methods: service.methods, + namer: namer + ) ) ), - .init(label: "context", type: .member(["GRPCCore", "ServerContext"])), - ], - keywords: [.async, .throws], - returnType: .identifierType( - .generic( - wrapper: .member(["GRPCCore", "ServerResponse", "Stream"]), - wrapped: .member(method.outputType) - ) - ) - ) - } - - 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 .guarded( - self.availabilityGuard, - .extension( - onType: streamingProtocol, - declarations: [registerRPCMethod] - ) - ) - } - - private func makeRegisterRPCsMethod( - for service: CodeGenerationRequest.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: CodeGenerationRequest.ServiceDescriptor, - in codeGenerationRequest: CodeGenerationRequest - ) -> [CodeBlock] { - let registerHandlerCalls = service.methods.compactMap { - CodeBlock.expression( - Expression.functionCall( - calledExpression: .memberAccess( - MemberAccessDescription(left: .identifierPattern("router"), right: "registerHandler") + // protocol ServiceProtocol { ... } + .commentable( + .preFormatted( + Docs.suffix( + self.serviceDocs(serviceName: service.name.identifyingName), + withDocs: service.documentation + ) ), - 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( - FunctionArgumentDescription( - label: "forMethod", - expression: .identifierPattern(self.methodDescriptorPath(for: method, service: service)) - ) - ) - - arguments.append( - FunctionArgumentDescription( - label: "deserializer", - expression: .identifierPattern(codeGenerationRequest.lookupDeserializer(method.inputType)) - ) - ) - - arguments.append( - FunctionArgumentDescription( - label: "serializer", - expression: .identifierPattern(codeGenerationRequest.lookupSerializer(method.outputType)) - ) - ) - - 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 handlerClosureBody = Expression.unaryKeyword( - kind: .try, - expression: .unaryKeyword(kind: .await, expression: rpcFunctionCall) - ) - - arguments.append( - FunctionArgumentDescription( - label: "handler", - expression: .closureInvocation( - ClosureInvocationDescription( - argumentNames: ["request", "context"], - 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( - .preFormatted(service.documentation), - .guarded( - self.availabilityGuard, - .protocol( - ProtocolDescription( - accessModifier: self.accessModifier, - name: protocolName, - conformances: [streamingProtocol], - members: methods - ) - ) - ) - ) - } - - private func makeServiceProtocolMethod( - for method: CodeGenerationRequest.ServiceDescriptor.MethodDescriptor, - in service: CodeGenerationRequest.ServiceDescriptor, - accessModifier: AccessModifier? = nil - ) -> Declaration { - let inputStreaming = method.isInputStreaming ? "Stream" : "Single" - let outputStreaming = method.isOutputStreaming ? "Stream" : "Single" - - let functionSignature = FunctionSignatureDescription( - accessModifier: accessModifier, - kind: .function(name: method.name.generatedLowerCase), - parameters: [ - .init( - label: "request", - type: - .generic( - wrapper: .member(["GRPCCore", "ServerRequest", inputStreaming]), - wrapped: .member(method.inputType) + .protocol( + .service( + accessLevel: accessModifier, + name: "ServiceProtocol", + streamingProtocol: "\(service.name.typeName).StreamingServiceProtocol", + methods: service.methods, + namer: namer ) + ) ), - .init(label: "context", type: .member(["GRPCCore", "ServerContext"])), - ], - keywords: [.async, .throws], - returnType: .identifierType( - .generic( - wrapper: .member(["GRPCCore", "ServerResponse", outputStreaming]), - wrapped: .member(method.outputType) - ) - ) - ) - - return .commentable( - .preFormatted(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 .guarded( - self.availabilityGuard, - .extension( - 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, - accessModifier: self.accessModifier - ), - 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("GRPCCore.ServerRequest"), - right: "Single" + // protocol SimpleServiceProtocol { ... } + .commentable( + .preFormatted( + Docs.suffix( + self.simpleServiceDocs(serviceName: service.name.identifyingName), + withDocs: service.documentation + ) + ), + .protocol( + .simpleServiceProtocol( + accessModifier: accessModifier, + name: "SimpleServiceProtocol", + serviceProtocol: "\(service.name.typeName).ServiceProtocol", + methods: service.methods, + namer: namer + ) ) ), - 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.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: 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(["GRPCCore", "ServerResponse"])), - right: "Stream" - ) + blocks.append(.declaration(.guarded(availability, .extension(`extension`)))) + + // extension .StreamingServiceProtocol> { ... } + let registerExtension: ExtensionDescription = .registrableRPCServiceDefaultImplementation( + accessLevel: accessModifier, + on: "\(service.name.typeName).StreamingServiceProtocol", + serviceNamespace: service.name.typeName, + methods: service.methods, + namer: namer, + serializer: serializer, + deserializer: deserializer + ) + blocks.append( + CodeBlock( + comment: .inline("Default implementation of 'registerMethods(with:)'."), + item: .declaration(.guarded(availability, .extension(registerExtension))) + ) + ) + + // extension _ServiceProtocol { ... } + let streamingServiceDefaultImplExtension: ExtensionDescription = + .streamingServiceProtocolDefaultImplementation( + accessModifier: accessModifier, + on: "\(service.name.typeName).ServiceProtocol", + methods: service.methods, + namer: namer + ) + blocks.append( + CodeBlock( + comment: .inline( + "Default implementation of streaming methods from 'StreamingServiceProtocol'." ), - arguments: [ - (FunctionArgumentDescription(label: "single", expression: .identifierPattern("response"))) - ] + item: .declaration(.guarded(availability, .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: CodeGenerationRequest.ServiceDescriptor.MethodDescriptor, - service: CodeGenerationRequest.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: CodeGenerationRequest.ServiceDescriptor, - streaming: Bool - ) -> String { - if streaming { - return "\(service.namespacedGeneratedName).StreamingServiceProtocol" - } - return "\(service.namespacedGeneratedName).ServiceProtocol" - } + ) - /// Generates the name of a service protocol. - internal func protocolName( - service: CodeGenerationRequest.ServiceDescriptor, - streaming: Bool - ) -> String { - if streaming { - return "\(service.namespacedGeneratedName)StreamingServiceProtocol" - } - return "\(service.namespacedGeneratedName)ServiceProtocol" + // extension _SimpleServiceProtocol { ... } + let serviceDefaultImplExtension: ExtensionDescription = .serviceProtocolDefaultImplementation( + accessModifier: accessModifier, + on: "\(service.name.typeName).SimpleServiceProtocol", + methods: service.methods, + namer: namer + ) + blocks.append( + CodeBlock( + comment: .inline("Default implementation of methods from 'ServiceProtocol'."), + item: .declaration(.guarded(availability, .extension(serviceDefaultImplExtension))) + ) + ) + + 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``. + """ + } + + 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/Sources/GRPCCodeGen/Internal/Translator/SpecializedTranslator.swift b/Sources/GRPCCodeGen/Internal/Translator/SpecializedTranslator.swift deleted file mode 100644 index 1db3fce02..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: SourceGenerator.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 36b1c665f..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: SourceGenerator.Config.AccessLevel, - accessLevelOnImports: Bool, - client: Bool, - server: Bool - ) throws -> StructuredSwiftRepresentation -} diff --git a/Sources/GRPCCodeGen/Internal/Translator/TypealiasTranslator.swift b/Sources/GRPCCodeGen/Internal/Translator/TypealiasTranslator.swift deleted file mode 100644 index a6bcc55ae..000000000 --- a/Sources/GRPCCodeGen/Internal/Translator/TypealiasTranslator.swift +++ /dev/null @@ -1,374 +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]() - 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)) - ) - ) - - codeBlocks.append( - CodeBlock(item: .declaration(self.makeServiceDescriptorExtension(for: service))) - ) - } - } - - return codeBlocks - } -} - -extension TypealiasTranslator { - private func makeServiceEnum( - from service: CodeGenerationRequest.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: CodeGenerationRequest.ServiceDescriptor.MethodDescriptor, - in service: CodeGenerationRequest.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: 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( - 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: CodeGenerationRequest.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: CodeGenerationRequest.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: CodeGenerationRequest.ServiceDescriptor - ) -> Declaration { - return .guarded( - self.availabilityGuard, - .typealias( - accessModifier: self.accessModifier, - name: "ClientProtocol", - existingType: .member("\(service.namespacedGeneratedName)ClientProtocol") - ) - ) - } - - private func makeClientStructTypealias( - for service: CodeGenerationRequest.ServiceDescriptor - ) -> Declaration { - return .guarded( - self.availabilityGuard, - .typealias( - accessModifier: self.accessModifier, - name: "Client", - existingType: .member("\(service.namespacedGeneratedName)Client") - ) - ) - } - - 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("GRPCCore.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: "GRPCCore.ServiceDescriptor", - declarations: [ - .variable( - VariableDescription( - accessModifier: self.accessModifier, - isStatic: true, - kind: .let, - left: .identifier(.pattern(serviceIdentifier)), - right: serviceDescriptorInitialization - ) - ) - ] - ) - ) - } -} 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. 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/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/GRPCCore/Call/Client/CallOptions.swift b/Sources/GRPCCore/Call/Client/CallOptions.swift index ce6388050..68a36a9e6 100644 --- a/Sources/GRPCCore/Call/Client/CallOptions.swift +++ b/Sources/GRPCCore/Call/Client/CallOptions.swift @@ -21,7 +21,7 @@ /// /// 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, *) +@available(gRPCSwift 2.0, *) public struct CallOptions: Sendable { /// The default timeout for the RPC. /// @@ -108,7 +108,7 @@ public struct CallOptions: Sendable { } } -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@available(gRPCSwift 2.0, *) extension CallOptions { /// Default call options. /// @@ -125,7 +125,7 @@ extension CallOptions { } } -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@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 51eaa1a21..b53983795 100644 --- a/Sources/GRPCCore/Call/Client/ClientContext.swift +++ b/Sources/GRPCCore/Call/Client/ClientContext.swift @@ -15,12 +15,50 @@ */ /// 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 + /// 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) { + /// + /// - 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, + localPeer: String + ) { self.descriptor = descriptor + self.remotePeer = remotePeer + self.localPeer = localPeer } } diff --git a/Sources/GRPCCore/Call/Client/ClientInterceptor.swift b/Sources/GRPCCore/Call/Client/ClientInterceptor.swift index 93ddf9cf5..a2e22ba87 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,12 +23,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. -/// -/// - TODO: Update example and documentation to show how to register an interceptor. +/// 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 +/// which RPC is being called and conditionalise behavior accordingly. /// /// Some examples of simple interceptors follow. /// @@ -40,13 +41,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 +66,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) /// @@ -88,7 +89,7 @@ /// ``` /// /// For server-side interceptors see ``ServerInterceptor``. -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +@available(gRPCSwift 2.0, *) public protocol ClientInterceptor: Sendable { /// Intercept a request object. /// @@ -100,11 +101,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..3e52a741c 100644 --- a/Sources/GRPCCore/Call/Client/ClientRequest.swift +++ b/Sources/GRPCCore/Call/Client/ClientRequest.swift @@ -14,91 +14,85 @@ * 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 requests +/// +/// ```swift +/// let request = ClientRequest(message: "Hello, gRPC!") +/// 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. /// - /// ```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(gRPCSwift 2.0, *) +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..23ec38546 100644 --- a/Sources/GRPCCore/Call/Client/ClientResponse.swift +++ b/Sources/GRPCCore/Call/Client/ClientResponse.swift @@ -14,255 +14,250 @@ * 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 +/// 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 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)'") +/// } +/// ``` +@available(gRPCSwift 2.0, *) +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 - /// 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 + /// 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. - /// - 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 `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. - /// - /// 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. + /// Whether the RPC was accepted or rejected. /// - /// 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 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: +/// - ``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(gRPCSwift 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. + /// Creates a ``Contents``. /// - /// 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 + /// - 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 { +@available(gRPCSwift 2.0, *) +extension ClientResponse { /// Creates a new accepted response. /// /// - Parameters: @@ -332,8 +327,8 @@ extension ClientResponse.Single { } } -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -extension ClientResponse.Stream { +@available(gRPCSwift 2.0, *) +extension StreamingClientResponse { /// Creates a new accepted response. /// /// - Parameters: @@ -372,7 +367,7 @@ extension ClientResponse.Stream { /// 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): @@ -391,4 +386,21 @@ extension ClientResponse.Stream { 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``. + @available(gRPCSwift 2.1, *) + public var bodyParts: RPCAsyncSequence { + switch self.accepted { + case let .success(contents): + return contents.bodyParts + + case let .failure(error): + return RPCAsyncSequence.throwing(error) + } + } } + +@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 bf57dae23..08bdb62d7 100644 --- a/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+HedgingExecutor.swift +++ b/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+HedgingExecutor.swift @@ -16,7 +16,7 @@ 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, *) +@available(gRPCSwift 2.0, *) extension ClientRPCExecutor { @usableFromInline struct HedgingExecutor< @@ -62,14 +62,14 @@ extension ClientRPCExecutor { } } -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +@available(gRPCSwift 2.0, *) 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: @@ -85,7 +85,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) } @@ -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,19 +312,19 @@ 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( descriptor: method, options: options - ) { stream -> _HedgingAttemptTaskResult.AttemptResult in + ) { stream, context -> _HedgingAttemptTaskResult.AttemptResult in return await withTaskGroup(of: _HedgingAttemptTaskResult.self) { group in group.addTask { do { @@ -350,8 +350,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, @@ -535,7 +535,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) @@ -545,7 +545,7 @@ extension ClientRPCExecutor.HedgingExecutor { } } -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@available(gRPCSwift 2.0, *) @usableFromInline enum _HedgingTaskResult: Sendable { case rpcHandled(Result) @@ -553,7 +553,7 @@ enum _HedgingTaskResult: Sendable { case timedOut(Result) } -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +@available(gRPCSwift 2.0, *) @usableFromInline enum _HedgingAttemptTaskResult: Sendable { case attemptPicked(Bool) @@ -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..97be8b77a 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 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +@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. @@ -54,14 +54,14 @@ extension ClientRPCExecutor { } } -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +@available(gRPCSwift 2.0, *) 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 @@ -90,22 +90,25 @@ extension ClientRPCExecutor.OneShotExecutor { } } -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +@available(gRPCSwift 2.0, *) 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 { - 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, @@ -129,7 +132,7 @@ extension ClientRPCExecutor.OneShotExecutor { } } -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +@available(gRPCSwift 2.0, *) @inlinable func withDeadline( _ deadline: ContinuousClock.Instant, @@ -138,7 +141,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 @@ -170,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 d808b1f4a..bb4eb4bc7 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 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +@available(gRPCSwift 2.0, *) extension ClientRPCExecutor { @usableFromInline struct RetryExecutor< @@ -60,14 +60,14 @@ extension ClientRPCExecutor { } } -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +@available(gRPCSwift 2.0, *) 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... // @@ -94,7 +94,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) } @@ -120,7 +120,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. @@ -129,6 +129,7 @@ extension ClientRPCExecutor.RetryExecutor { } return await self.executeAttempt( + context: context, stream: stream, metadata: metadata, retryStream: retry.stream, @@ -156,11 +157,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 ) } @@ -195,26 +201,30 @@ extension ClientRPCExecutor.RetryExecutor { } @inlinable - func executeAttempt( - stream: RPCStream, + func executeAttempt( + context: ClientContext, + stream: RPCStream< + RPCAsyncSequence, any Error>, + RPCWriter>.Closable + >, metadata: Metadata, 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) } let response = await ClientRPCExecutor._execute( in: &group, + context: context, request: request, - method: method, attempt: attempt, serializer: self.serializer, deserializer: self.deserializer, @@ -302,7 +312,7 @@ extension ClientRPCExecutor.RetryExecutor { } } -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@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 33e46fd2d..f7e41fad9 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 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +@available(gRPCSwift 2.0, *) @usableFromInline enum ClientRPCExecutor { /// Execute the request and handle its response. @@ -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 } @@ -100,31 +100,34 @@ enum ClientRPCExecutor { } } -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +@available(gRPCSwift 2.0, *) extension ClientRPCExecutor { /// Executes a request on a given stream processor. /// /// - 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( + static func _execute( in group: inout TaskGroup, - request: ClientRequest.Stream, - method: MethodDescriptor, + context: ClientContext, + request: StreamingClientRequest, attempt: Int, serializer: some MessageSerializer, deserializer: some MessageDeserializer, interceptors: [any ClientInterceptor], - stream: RPCStream - ) async -> ClientResponse.Stream { - let context = ClientContext(descriptor: method) + stream: RPCStream< + RPCAsyncSequence, any Error>, + RPCWriter>.Closable + > + ) async -> StreamingClientResponse { if interceptors.isEmpty { return await ClientStreamExecutor.execute( @@ -159,15 +162,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 +187,12 @@ extension ClientRPCExecutor { ) } } catch let error as RPCError { - return ClientResponse.Stream(error: error) + return StreamingClientResponse(error: error) + } catch let error as any RPCErrorConvertible { + return StreamingClientResponse(error: RPCError(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..3a6b15969 100644 --- a/Sources/GRPCCore/Call/Client/Internal/ClientRequest+Convenience.swift +++ b/Sources/GRPCCore/Call/Client/Internal/ClientRequest+Convenience.swift @@ -14,9 +14,9 @@ * limitations under the License. */ -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -extension ClientRequest.Stream { - internal init(single request: ClientRequest.Single) { +@available(gRPCSwift 2.0, *) +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..41c3d0244 100644 --- a/Sources/GRPCCore/Call/Client/Internal/ClientResponse+Convenience.swift +++ b/Sources/GRPCCore/Call/Client/Internal/ClientResponse+Convenience.swift @@ -14,12 +14,12 @@ * limitations under the License. */ -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -extension ClientResponse.Single { +@available(gRPCSwift 2.0, *) +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 { @@ -82,8 +82,8 @@ extension ClientResponse.Single { } } -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -extension ClientResponse.Stream { +@available(gRPCSwift 2.0, *) +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 @@ -103,8 +103,8 @@ extension ClientResponse.Stream { } } -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -extension ClientResponse.Stream { +@available(gRPCSwift 2.0, *) +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..74aac103f 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 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +@available(gRPCSwift 2.0, *) @usableFromInline internal enum ClientStreamExecutor { /// Execute a request on the stream executor. @@ -29,15 +29,18 @@ 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: ClientRequest.Stream, + request: StreamingClientRequest, context: ClientContext, attempt: Int, serializer: some MessageSerializer, deserializer: some MessageDeserializer, - stream: RPCStream - ) async -> ClientResponse.Stream { + stream: RPCStream< + RPCAsyncSequence, any Error>, + RPCWriter>.Closable + > + ) async -> StreamingClientResponse { // Let the server know this is a retry. var metadata = request.metadata if attempt > 1 { @@ -63,7 +66,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 +78,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, + static func _processRequest( + on stream: some ClosableRPCWriterProtocol>, + request: StreamingClientRequest, serializer: some MessageSerializer ) async { let result = await Result { @@ -105,16 +108,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,9 +171,9 @@ internal enum ClientStreamExecutor { } @usableFromInline - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) struct RawBodyPartToMessageSequence< - Base: AsyncSequence, + Base: AsyncSequence, Failure>, + Bytes: GRPCContiguousBytes, Message: Sendable, Deserializer: MessageDeserializer, Failure: Error @@ -194,7 +200,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 +216,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 +244,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/Client/Internal/RetryDelaySequence.swift b/Sources/GRPCCore/Call/Client/Internal/RetryDelaySequence.swift index d1ef417f3..b9f54a916 100644 --- a/Sources/GRPCCore/Call/Client/Internal/RetryDelaySequence.swift +++ b/Sources/GRPCCore/Call/Client/Internal/RetryDelaySequence.swift @@ -15,11 +15,17 @@ */ #if canImport(Darwin) public import Darwin // should be @usableFromInline -#else +#elseif canImport(Android) +public import Android // should be @usableFromInline +#elseif canImport(Glibc) public import Glibc // should be @usableFromInline +#elseif canImport(Musl) +public import Musl // should be @usableFromInline +#else +#error("Unsupported OS") #endif -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@available(gRPCSwift 2.0, *) @usableFromInline struct RetryDelaySequence: Sequence { @usableFromInline diff --git a/Sources/GRPCCore/Call/ConditionalInterceptor.swift b/Sources/GRPCCore/Call/ConditionalInterceptor.swift new file mode 100644 index 000000000..c5302260c --- /dev/null +++ b/Sources/GRPCCore/Call/ConditionalInterceptor.swift @@ -0,0 +1,115 @@ +/* + * Copyright 2024, gRPC Authors All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 conditions under which an interceptor should be applied. +/// +/// 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`` 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 { + 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. + 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. + public static func methods(_ methods: Set) -> Self { + Self(wrapped: .methods(methods)) + } + + @usableFromInline + package 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. + public let interceptor: Interceptor + + @usableFromInline + internal let subject: Subject + + fileprivate init(interceptor: Interceptor, subject: Subject) { + self.interceptor = interceptor + self.subject = 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) + } +} + +@available(gRPCSwift 2.0, *) +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) + } +} + +@available(gRPCSwift 2.0, *) +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/Internal/ServerCancellationManager.swift b/Sources/GRPCCore/Call/Server/Internal/ServerCancellationManager.swift new file mode 100644 index 000000000..d4b8021fc --- /dev/null +++ b/Sources/GRPCCore/Call/Server/Internal/ServerCancellationManager.swift @@ -0,0 +1,256 @@ +/* + * Copyright 2024, gRPC Authors All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 . +@available(gRPCSwift 2.0, *) +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() + } + } +} + +@available(gRPCSwift 2.0, *) +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 a67bfbe37..8df70f86d 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 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +@available(gRPCSwift 2.0, *) @usableFromInline struct ServerRPCExecutor { /// Executes an RPC using the provided handler. @@ -24,22 +24,23 @@ 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( + static func execute( context: ServerContext, stream: RPCStream< - RPCAsyncSequence, - RPCWriter.Closable + RPCAsyncSequence, any Error>, + RPCWriter>.Closable >, deserializer: some MessageDeserializer, 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) @@ -66,18 +67,18 @@ 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], 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( @@ -106,73 +107,59 @@ 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], 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 + await withTaskGroup(of: Void.self) { group in group.addTask { - let result = await Result { - try await Task.sleep(for: timeout, clock: .continuous) + do { + try await Task.sleep(for: timeout, tolerance: .zero, 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() } } @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], 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 +179,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) ), @@ -202,7 +189,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 } @@ -241,12 +236,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)): @@ -281,10 +276,10 @@ struct ServerRPCExecutor { } @usableFromInline - enum OnFirstRequestPart { + enum OnFirstRequestPart { case process( Metadata, - UnsafeTransfer.AsyncIterator> + UnsafeTransfer, any Error>.AsyncIterator> ) case reject(RPCError) } @@ -296,18 +291,18 @@ struct ServerRPCExecutor { } } -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +@available(gRPCSwift 2.0, *) 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 +313,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 +331,12 @@ 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 error as any RPCErrorConvertible { + return StreamingServerResponse(error: RPCError(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..88c89c635 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 @@ -32,16 +34,16 @@ /// 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 { +@available(gRPCSwift 2.0, *) +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] @@ -53,9 +55,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( @@ -72,8 +74,8 @@ public struct RPCRouter: Sendable { @inlinable func handle( stream: RPCStream< - RPCAsyncSequence, - RPCWriter.Closable + RPCAsyncSequence, any Error>, + RPCWriter>.Closable >, context: ServerContext, interceptors: [any ServerInterceptor] @@ -83,7 +85,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() { @@ -123,16 +126,17 @@ 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( + let handler = RPCHandler( method: descriptor, deserializer: deserializer, serializer: serializer, handler: handler ) + self.handlers[descriptor] = (handler, []) } /// Removes any handler registered for the specified method. @@ -143,19 +147,38 @@ 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. + @inlinable + public mutating func registerInterceptors( + pipeline: [ConditionalInterceptor] + ) { + 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 } + } + } + } } -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +@available(gRPCSwift 2.0, *) extension RPCRouter { internal func handle( stream: RPCStream< - RPCAsyncSequence, - RPCWriter.Closable + RPCAsyncSequence, any Error>, + 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 @@ -166,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 d9236c75b..7eed9c647 100644 --- a/Sources/GRPCCore/Call/Server/RegistrableRPCService.swift +++ b/Sources/GRPCCore/Call/Server/RegistrableRPCService.swift @@ -22,10 +22,11 @@ /// 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, *) +@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``. /// /// - 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/Call/Server/ServerContext+RPCCancellationHandle.swift b/Sources/GRPCCore/Call/Server/ServerContext+RPCCancellationHandle.swift new file mode 100644 index 000000000..607c478c6 --- /dev/null +++ b/Sources/GRPCCore/Call/Server/ServerContext+RPCCancellationHandle.swift @@ -0,0 +1,120 @@ +/* + * Copyright 2024, gRPC Authors All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 + +@available(gRPCSwift 2.0, *) +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. +@available(gRPCSwift 2.0, *) +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. +@available(gRPCSwift 2.0, *) +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..6df188f1c 100644 --- a/Sources/GRPCCore/Call/Server/ServerContext.swift +++ b/Sources/GRPCCore/Call/Server/ServerContext.swift @@ -15,12 +15,72 @@ */ /// 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. 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 + + /// 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 + @available(gRPCSwift 2.2, *) + public var transportSpecific: (any TransportSpecific)? + + /// 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. + /// - 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, + remotePeer: String, + localPeer: String, + cancellation: RPCCancellationHandle + ) { self.descriptor = descriptor + self.remotePeer = remotePeer + self.localPeer = localPeer + self.cancellation = cancellation } } diff --git a/Sources/GRPCCore/Call/Server/ServerInterceptor.swift b/Sources/GRPCCore/Call/Server/ServerInterceptor.swift index 2243fe8f2..8192fc21e 100644 --- a/Sources/GRPCCore/Call/Server/ServerInterceptor.swift +++ b/Sources/GRPCCore/Call/Server/ServerInterceptor.swift @@ -21,12 +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 -/// ``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. +/// 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 +/// which RPC is being called and conditionalise behavior accordingly. /// /// ## RPC filtering /// @@ -35,19 +34,19 @@ /// demonstrates this. /// /// ```swift -/// struct AuthServerInterceptor: Sendable { +/// struct AuthServerInterceptor: ServerInterceptor { /// let isAuthorized: @Sendable (String, MethodDescriptor) async throws -> Void /// /// func intercept( -/// request: ServerRequest.Stream, -/// context: ServerInterceptorContext, +/// request: StreamingServerRequest, +/// context: ServerContext, /// next: @Sendable ( -/// _ request: ServerRequest.Stream, -/// _ context: ServerInterceptorContext -/// ) async throws -> ServerResponse.Stream -/// ) async throws -> ServerResponse.Stream { +/// _ request: StreamingServerRequest, +/// _ 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") /// } /// @@ -61,7 +60,7 @@ /// ``` /// /// For client-side interceptors see ``ClientInterceptor``. -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +@available(gRPCSwift 2.0, *) public protocol ServerInterceptor: Sendable { /// Intercept a request object. /// @@ -73,11 +72,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..6dd23063b 100644 --- a/Sources/GRPCCore/Call/Server/ServerRequest.swift +++ b/Sources/GRPCCore/Call/Server/ServerRequest.swift @@ -14,72 +14,66 @@ * 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. +@available(gRPCSwift 2.0, *) +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. +@available(gRPCSwift 2.0, *) +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) { +@available(gRPCSwift 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.Single { - public init(stream request: ServerRequest.Stream) async throws { +@available(gRPCSwift 2.0, *) +extension ServerRequest { + public init(stream request: StreamingServerRequest) async throws { var iterator = request.messages.makeAsyncIterator() guard let message = try await iterator.next() else { @@ -90,6 +84,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..0b439355d 100644 --- a/Sources/GRPCCore/Call/Server/ServerResponse.swift +++ b/Sources/GRPCCore/Call/Server/ServerResponse.swift @@ -14,220 +14,215 @@ * 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 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)'") +/// } +/// ``` +@available(gRPCSwift 2.0, *) +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``. - /// - /// 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: + /// Whether the RPC was accepted or rejected. /// - /// ```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") + /// 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. /// - /// // 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 + /// - Parameter accepted: Whether the RPC was accepted or rejected. + public init(accepted: Result) { + self.accepted = accepted + } +} - /// 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 +/// 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 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: +/// - ``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(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 { + /// Metadata to send to the client at the beginning of the response stream. + public var metadata: 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. + /// 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 { +@available(gRPCSwift 2.0, *) +extension ServerResponse { /// Creates a new accepted response. /// /// - Parameters: @@ -252,15 +247,25 @@ extension ServerResponse.Single { 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) + } } } @@ -286,8 +291,8 @@ extension ServerResponse.Single { } } -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -extension ServerResponse.Stream { +@available(gRPCSwift 2.0, *) +extension StreamingServerResponse { /// Creates a new accepted response. /// /// - Parameters: @@ -312,22 +317,32 @@ extension ServerResponse.Stream { 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) + } } } } -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -extension ServerResponse.Stream { - public init(single response: ServerResponse.Single) { +@available(gRPCSwift 2.0, *) +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/Coding/Coding.swift b/Sources/GRPCCore/Coding/Coding.swift index 29569d3c0..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 @@ -30,7 +31,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. @@ -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 @@ -49,5 +51,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/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 new file mode 100644 index 000000000..79930ab01 --- /dev/null +++ b/Sources/GRPCCore/Coding/GRPCContiguousBytes.swift @@ -0,0 +1,60 @@ +/* + * 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. +@available(gRPCSwift 2.0, *) +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 +} + +@available(gRPCSwift 2.0, *) +extension [UInt8]: GRPCContiguousBytes {} diff --git a/Sources/GRPCCore/Configuration/MethodConfig.swift b/Sources/GRPCCore/Configuration/MethodConfig.swift index 295d049a3..3decbfe58 100644 --- a/Sources/GRPCCore/Configuration/MethodConfig.swift +++ b/Sources/GRPCCore/Configuration/MethodConfig.swift @@ -16,9 +16,10 @@ /// 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, *) +/// 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 { /// The name of the service, including the namespace. /// @@ -144,7 +145,8 @@ public struct MethodConfig: Hashable, Sendable { } } -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +/// Whether an RPC should be retried or hedged. +@available(gRPCSwift 2.0, *) public struct RPCExecutionPolicy: Hashable, Sendable { @usableFromInline enum Wrapped: Hashable, Sendable { @@ -213,8 +215,8 @@ 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). -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +/// 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,8 +333,8 @@ 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). -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +/// 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. /// @@ -386,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( @@ -397,23 +400,7 @@ 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"), - 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, *) +@available(gRPCSwift 2.0, *) extension MethodConfig: Codable { private enum CodingKeys: String, CodingKey { case name @@ -472,7 +459,7 @@ extension MethodConfig: Codable { } } -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@available(gRPCSwift 2.0, *) extension MethodConfig.Name: Codable { private enum CodingKeys: String, CodingKey { case service @@ -498,7 +485,7 @@ extension MethodConfig.Name: Codable { } } -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@available(gRPCSwift 2.0, *) extension RetryPolicy: Codable { private enum CodingKeys: String, CodingKey { case maxAttempts @@ -514,12 +501,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) @@ -546,7 +533,7 @@ extension RetryPolicy: Codable { } } -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@available(gRPCSwift 2.0, *) extension HedgingPolicy: Codable { private enum CodingKeys: String, CodingKey { case maxAttempts @@ -560,8 +547,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 }) @@ -578,7 +565,7 @@ extension HedgingPolicy: Codable { } } -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@available(gRPCSwift 2.0, *) struct GoogleProtobufDuration: Codable { var duration: Duration @@ -616,6 +603,7 @@ struct GoogleProtobufDuration: Codable { } } +@available(gRPCSwift 2.0, *) struct GoogleRPCCode: Codable { var code: Status.Code @@ -648,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 f0e41c4bc..a96119f1e 100644 --- a/Sources/GRPCCore/Configuration/ServiceConfig.swift +++ b/Sources/GRPCCore/Configuration/ServiceConfig.swift @@ -16,8 +16,13 @@ /// 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, *) +/// 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. +@available(gRPCSwift 2.0, *) public struct ServiceConfig: Hashable, Sendable { /// Per-method configuration. public var methodConfig: [MethodConfig] @@ -63,7 +68,7 @@ public struct ServiceConfig: Hashable, Sendable { } } -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@available(gRPCSwift 2.0, *) extension ServiceConfig: Codable { private enum CodingKeys: String, CodingKey { case methodConfig @@ -100,7 +105,7 @@ extension ServiceConfig: Codable { } } -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@available(gRPCSwift 2.0, *) extension ServiceConfig { /// Configuration used by clients for load-balancing. public struct LoadBalancingConfig: Hashable, Sendable { @@ -166,7 +171,7 @@ extension ServiceConfig { } } -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@available(gRPCSwift 2.0, *) extension ServiceConfig.LoadBalancingConfig { /// Configuration for the pick-first load balancing policy. public struct PickFirst: Hashable, Sendable, Codable { @@ -194,7 +199,7 @@ extension ServiceConfig.LoadBalancingConfig { } } -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@available(gRPCSwift 2.0, *) extension ServiceConfig.LoadBalancingConfig: Codable { private enum CodingKeys: String, CodingKey { case roundRobin = "round_robin" @@ -225,7 +230,7 @@ extension ServiceConfig.LoadBalancingConfig: Codable { } } -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@available(gRPCSwift 2.0, *) extension ServiceConfig { public struct RetryThrottling: Hashable, Sendable, Codable { /// The initial, and maximum number of tokens. diff --git a/Sources/GRPCCore/Documentation.docc/Articles/Compatibility.md b/Sources/GRPCCore/Documentation.docc/Articles/Compatibility.md new file mode 100644 index 000000000..5862c17ca --- /dev/null +++ b/Sources/GRPCCore/Documentation.docc/Articles/Compatibility.md @@ -0,0 +1,33 @@ +# 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. + +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 | diff --git a/Sources/GRPCCore/Documentation.docc/Articles/Error-handling.md b/Sources/GRPCCore/Documentation.docc/Articles/Error-handling.md new file mode 100644 index 000000000..5addfc7b4 --- /dev/null +++ b/Sources/GRPCCore/Documentation.docc/Articles/Error-handling.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/Articles/Generating-stubs.md b/Sources/GRPCCore/Documentation.docc/Articles/Generating-stubs.md index 34b57f8d2..d47ab1997 100644 --- a/Sources/GRPCCore/Documentation.docc/Articles/Generating-stubs.md +++ b/Sources/GRPCCore/Documentation.docc/Articles/Generating-stubs.md @@ -1,204 +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 -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.) | -| `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. - -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`. +You can learn about generating gRPC stubs from the [gRPC Swift Protobuf +documentation](https://swiftpackageindex.com/grpc/grpc-swift-protobuf/documentation/grpcprotobuf/generating-stubs). 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/Articles/Public-API.md b/Sources/GRPCCore/Documentation.docc/Articles/Public-API.md new file mode 100644 index 000000000..5a24ad28a --- /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 are 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/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 new file mode 100644 index 000000000..8175f036f --- /dev/null +++ b/Sources/GRPCCore/Documentation.docc/Development/Design.md @@ -0,0 +1,448 @@ +# 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). + +## Overview + +### 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/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 +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: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 +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/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/runConnections()`` 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 de33f0bb1..3c5f6b08d 100644 --- a/Sources/GRPCCore/Documentation.docc/Documentation.md +++ b/Sources/GRPCCore/Documentation.docc/Documentation.md @@ -2,22 +2,42 @@ 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 +### 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: +gRPC Swift is distributed across multiple Swift packages. These are: -- ``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` (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 @@ -29,9 +49,96 @@ contains products of this package: ### Essentials - +- + +### Project Information + +- +- +- ### Getting involved 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`` +- ``ConditionalInterceptor`` + +### 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/Documentation.docc/Tutorials/Hello-World/Hello-World.tutorial b/Sources/GRPCCore/Documentation.docc/Tutorials/Hello-World/Hello-World.tutorial index 32ecd4847..a16c4edb2 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,27 +18,38 @@ repository by running the following command in a terminal: ```console - git clone https://github.com/grpc/grpc-swift + git clone --branch 2.0.0 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") { 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") } @@ -60,8 +71,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") } @@ -76,21 +86,21 @@ } @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 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 { - Open `Serve.swift` in the `Examples/v2/hello-world/Subcommands` directory. + 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. @Code(name: "Serve.swift", file: "hello-world-sec04-step01.swift") } @@ -102,8 +112,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") } @@ -116,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/Hello-World/Resources/hello-world-sec04-step01.swift b/Sources/GRPCCore/Documentation.docc/Tutorials/Hello-World/Resources/hello-world-sec04-step01.swift index 2c89bab8d..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,10 +1,11 @@ -struct Greeter: Helloworld_GreeterServiceProtocol { +struct Greeter: Helloworld_Greeter.SimpleServiceProtocol { func sayHello( - request: ServerRequest.Single - ) async throws -> ServerResponse.Single { + request: Helloworld_HelloRequest, + context: ServerContext + ) async throws -> Helloworld_HelloReply { var reply = Helloworld_HelloReply() let recipient = request.message.name.isEmpty ? "stranger" : request.message.name reply.message = "Hello, \(recipient)" - return ServerResponse.Single(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 e9dde27f9..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,19 +1,21 @@ -struct Greeter: Helloworld_GreeterServiceProtocol { +struct Greeter: Helloworld_Greeter.SimpleServiceProtocol { func sayHello( - request: ServerRequest.Single - ) async throws -> ServerResponse.Single { + request: Helloworld_HelloRequest, + context: ServerContext + ) async throws -> Helloworld_HelloReply { var reply = Helloworld_HelloReply() let recipient = request.message.name.isEmpty ? "stranger" : request.message.name reply.message = "Hello, \(recipient)" - return ServerResponse.Single(message: reply) + return reply } func sayHelloAgain( - request: ServerRequest.Single - ) async throws -> ServerResponse.Single { + request: Helloworld_HelloRequest, + context: ServerContext + ) 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.Single(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 baaffab21..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,8 +5,9 @@ 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"), + .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 c9f71095f..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,16 +5,17 @@ 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"), + .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( name: "RouteGuide", dependencies: [ - .product(name: "_GRPCHTTP2Transport", package: "grpc-swift"), - .product(name: "_GRPCProtobuf", package: "grpc-swift"), - .product(name: "SwiftProtobuf", package: "swift-protobuf"), + .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-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..427915609 --- /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"), + .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( + 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-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-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 de6f415cd..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,23 +1,29 @@ import GRPCCore -struct RouteGuideService: Routeguide_RouteGuide.ServiceProtocol { +struct RouteGuideService: Routeguide_RouteGuide.SimpleServiceProtocol { func getFeature( - request: ServerRequest.Single - ) async throws -> ServerResponse.Single { + request: Routeguide_Point, + context: ServerContext + ) async throws -> Routeguide_Feature { } func listFeatures( - request: ServerRequest.Single - ) async throws -> ServerResponse.Stream { + request: Routeguide_Rectangle, + response: RPCWriter, + context: ServerContext + ) async throws { } func recordRoute( - request: ServerRequest.Stream - ) async throws -> ServerResponse.Single { + request: RPCAsyncSequence, + context: ServerContext + ) async throws -> Routeguide_RouteSummary { } func routeChat( - request: ServerRequest.Stream - ) async throws -> ServerResponse.Stream { + request: RPCAsyncSequence, + response: RPCWriter, + context: ServerContext + ) 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 99bb69ad7..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,22 +11,28 @@ struct RouteGuideService: Routeguide_RouteGuide.ServiceProtocol { } func getFeature( - request: ServerRequest.Single - ) async throws -> ServerResponse.Single { + request: Routeguide_Point, + context: ServerContext + ) async throws -> Routeguide_Feature { } func listFeatures( - request: ServerRequest.Single - ) async throws -> ServerResponse.Stream { + request: Routeguide_Rectangle, + response: RPCWriter, + context: ServerContext + ) async throws { } func recordRoute( - request: ServerRequest.Stream - ) async throws -> ServerResponse.Single { + request: RPCAsyncSequence, + context: ServerContext + ) async throws -> Routeguide_RouteSummary { } func routeChat( - request: ServerRequest.Stream - ) async throws -> ServerResponse.Stream { + request: RPCAsyncSequence, + response: RPCWriter, + context: ServerContext + ) 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 cf791d6f8..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,8 +18,9 @@ struct RouteGuideService: Routeguide_RouteGuide.ServiceProtocol { } func getFeature( - request: ServerRequest.Single - ) async throws -> ServerResponse.Single { + request: Routeguide_Point, + context: ServerContext + ) async throws -> Routeguide_Feature { let feature = self.findFeature( latitude: request.message.latitude, longitude: request.message.longitude @@ -27,17 +28,22 @@ struct RouteGuideService: Routeguide_RouteGuide.ServiceProtocol { } func listFeatures( - request: ServerRequest.Single - ) async throws -> ServerResponse.Stream { + request: Routeguide_Rectangle, + response: RPCWriter, + context: ServerContext + ) async throws { } func recordRoute( - request: ServerRequest.Stream - ) async throws -> ServerResponse.Single { + request: RPCAsyncSequence, + context: ServerContext + ) async throws -> Routeguide_RouteSummary { } func routeChat( - request: ServerRequest.Stream - ) async throws -> ServerResponse.Stream { + request: RPCAsyncSequence, + response: RPCWriter, + context: ServerContext + ) 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 452c74a85..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,15 +18,16 @@ struct RouteGuideService: Routeguide_RouteGuide.ServiceProtocol { } func getFeature( - request: ServerRequest.Single - ) async throws -> ServerResponse.Single { + request: Routeguide_Point, + context: ServerContext + ) async throws -> Routeguide_Feature { let feature = self.findFeature( latitude: request.message.latitude, longitude: request.message.longitude ) if let feature { - return ServerResponse.Single(message: feature) + return feature } else { // No feature: return a feature with an empty name. let unknownFeature = Routeguide_Feature.with { @@ -36,22 +37,27 @@ struct RouteGuideService: Routeguide_RouteGuide.ServiceProtocol { $0.longitude = request.message.longitude } } - return ServerResponse.Single(message: unknownFeature) + return unknownFeature } } func listFeatures( - request: ServerRequest.Single - ) async throws -> ServerResponse.Stream { + request: Routeguide_Rectangle, + response: RPCWriter, + context: ServerContext + ) async throws { } func recordRoute( - request: ServerRequest.Stream - ) async throws -> ServerResponse.Single { + request: RPCAsyncSequence, + context: ServerContext + ) async throws -> Routeguide_RouteSummary { } func routeChat( - request: ServerRequest.Stream - ) async throws -> ServerResponse.Stream { + request: RPCAsyncSequence, + response: RPCWriter, + context: ServerContext + ) 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 b52288fa3..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,15 +18,16 @@ struct RouteGuideService: Routeguide_RouteGuide.ServiceProtocol { } func getFeature( - request: ServerRequest.Single - ) async throws -> ServerResponse.Single { + request: Routeguide_Point, + context: ServerContext + ) async throws -> Routeguide_Feature { let feature = self.findFeature( latitude: request.message.latitude, longitude: request.message.longitude ) if let feature { - return ServerResponse.Single(message: feature) + return feature } else { // No feature: return a feature with an empty name. let unknownFeature = Routeguide_Feature.with { @@ -36,30 +37,33 @@ struct RouteGuideService: Routeguide_RouteGuide.ServiceProtocol { $0.longitude = request.message.longitude } } - return ServerResponse.Single(message: unknownFeature) + return 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) - } + request: Routeguide_Rectangle, + response: RPCWriter, + context: ServerContext + ) async throws { + for feature in self.features { + if !feature.name.isEmpty, feature.isContained(by: request) { + try await response.write(feature) } } } func recordRoute( - request: ServerRequest.Stream - ) async throws -> ServerResponse.Single { + request: RPCAsyncSequence, + context: ServerContext + ) async throws -> Routeguide_RouteSummary { } func routeChat( - request: ServerRequest.Stream - ) async throws -> ServerResponse.Stream { + request: RPCAsyncSequence, + response: RPCWriter, + context: ServerContext + ) 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 780c63a14..000000000 --- a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec04-step07-server-streaming.swift +++ /dev/null @@ -1,75 +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.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 index 76f399c50..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,15 +19,16 @@ struct RouteGuideService: Routeguide_RouteGuide.ServiceProtocol { } func getFeature( - request: ServerRequest.Single - ) async throws -> ServerResponse.Single { + request: Routeguide_Point, + context: ServerContext + ) async throws -> Routeguide_Feature { let feature = self.findFeature( latitude: request.message.latitude, longitude: request.message.longitude ) if let feature { - return ServerResponse.Single(message: feature) + return feature } else { // No feature: return a feature with an empty name. let unknownFeature = Routeguide_Feature.with { @@ -37,34 +38,33 @@ struct RouteGuideService: Routeguide_RouteGuide.ServiceProtocol { $0.longitude = request.message.longitude } } - return ServerResponse.Single(message: unknownFeature) + return 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) - } + request: Routeguide_Rectangle, + response: RPCWriter, + context: ServerContext + ) async throws { + for feature in self.features { + if !feature.name.isEmpty, feature.isContained(by: request) { + try await response.write(feature) } - - return [:] } } func recordRoute( - request: ServerRequest.Stream - ) async throws -> ServerResponse.Single { + request: RPCAsyncSequence, + context: ServerContext + ) 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 { @@ -86,12 +86,14 @@ struct RouteGuideService: Routeguide_RouteGuide.ServiceProtocol { $0.distance = Int32(distanceTravelled) } - return ServerResponse.Single(message: summary) + return summary } func routeChat( - request: ServerRequest.Stream - ) async throws -> ServerResponse.Stream { + request: RPCAsyncSequence, + response: RPCWriter, + context: ServerContext + ) 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 5c8ad560d..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,15 +20,16 @@ struct RouteGuideService: Routeguide_RouteGuide.ServiceProtocol { } func getFeature( - request: ServerRequest.Single - ) async throws -> ServerResponse.Single { + request: Routeguide_Point, + context: ServerContext + ) async throws -> Routeguide_Feature { let feature = self.findFeature( latitude: request.message.latitude, longitude: request.message.longitude ) if let feature { - return ServerResponse.Single(message: feature) + return feature } else { // No feature: return a feature with an empty name. let unknownFeature = Routeguide_Feature.with { @@ -68,34 +39,33 @@ struct RouteGuideService: Routeguide_RouteGuide.ServiceProtocol { $0.longitude = request.message.longitude } } - return ServerResponse.Single(message: unknownFeature) + return 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) - } + request: Routeguide_Rectangle, + response: RPCWriter, + context: ServerContext + ) async throws { + for feature in self.features { + if !feature.name.isEmpty, feature.isContained(by: request) { + try await response.write(feature) } - - return [:] } } func recordRoute( - request: ServerRequest.Stream - ) async throws -> ServerResponse.Single { + request: RPCAsyncSequence, + context: ServerContext + ) 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 { @@ -117,12 +87,14 @@ struct RouteGuideService: Routeguide_RouteGuide.ServiceProtocol { $0.distance = Int32(distanceTravelled) } - return ServerResponse.Single(message: summary) + return summary } func routeChat( - request: ServerRequest.Stream - ) async throws -> ServerResponse.Stream { + request: RPCAsyncSequence, + response: RPCWriter, + context: ServerContext + ) 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 3913b0c36..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,15 +20,16 @@ struct RouteGuideService: Routeguide_RouteGuide.ServiceProtocol { } func getFeature( - request: ServerRequest.Single - ) async throws -> ServerResponse.Single { + request: Routeguide_Point, + context: ServerContext + ) async throws -> Routeguide_Feature { let feature = self.findFeature( latitude: request.message.latitude, longitude: request.message.longitude ) if let feature { - return ServerResponse.Single(message: feature) + return feature } else { // No feature: return a feature with an empty name. let unknownFeature = Routeguide_Feature.with { @@ -68,34 +39,33 @@ struct RouteGuideService: Routeguide_RouteGuide.ServiceProtocol { $0.longitude = request.message.longitude } } - return ServerResponse.Single(message: unknownFeature) + return 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) - } + request: Routeguide_Rectangle, + response: RPCWriter, + context: ServerContext + ) async throws { + for feature in self.features { + if !feature.name.isEmpty, feature.isContained(by: request) { + try await response.write(feature) } - - return [:] } } func recordRoute( - request: ServerRequest.Stream - ) async throws -> ServerResponse.Single { + request: RPCAsyncSequence, + context: ServerContext + ) 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 { @@ -117,18 +87,17 @@ struct RouteGuideService: Routeguide_RouteGuide.ServiceProtocol { $0.distance = Int32(distanceTravelled) } - return ServerResponse.Single(message: summary) + return 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 [:] + request: RPCAsyncSequence, + response: RPCWriter, + context: ServerContext + ) 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 c9f71095f..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,16 +5,20 @@ 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"), + .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( name: "RouteGuide", dependencies: [ - .product(name: "_GRPCHTTP2Transport", package: "grpc-swift"), - .product(name: "_GRPCProtobuf", package: "grpc-swift"), - .product(name: "SwiftProtobuf", package: "swift-protobuf"), + .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 05447cfb9..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,19 +5,23 @@ 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"), + .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( name: "RouteGuide", dependencies: [ - .product(name: "_GRPCHTTP2Transport", package: "grpc-swift"), - .product(name: "_GRPCProtobuf", package: "grpc-swift"), - .product(name: "SwiftProtobuf", package: "swift-protobuf"), + .product(name: "GRPCCore", package: "grpc-swift"), + .product(name: "GRPCNIOTransportHTTP2", package: "grpc-swift-nio-transport"), + .product(name: "GRPCProtobuf", package: "grpc-swift-protobuf"), ], 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 1a49f098d..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,21 +5,25 @@ 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"), + .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: [ .executableTarget( name: "RouteGuide", dependencies: [ - .product(name: "_GRPCHTTP2Transport", package: "grpc-swift"), - .product(name: "_GRPCProtobuf", package: "grpc-swift"), - .product(name: "SwiftProtobuf", package: "swift-protobuf"), + .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"), ], 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-step07-server.swift b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step07-server.swift index b3ffcaf33..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 @@ -1,4 +1,5 @@ -import GRPCHTTP2Transport +import GRPCCore +import GRPCNIOTransportHTTP2 extension RouteGuide { func runServer() async throws { @@ -7,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 b99885335..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 @@ -1,4 +1,5 @@ -import GRPCHTTP2Transport +import GRPCCore +import GRPCNIOTransportHTTP2 extension RouteGuide { func runServer() async throws { @@ -7,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-step02-add-run-client.swift b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step02-add-run-client.swift index eee2b6838..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 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 index ac97861b9..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 @@ -1,12 +1,15 @@ -import GRPCHTTP2Transport +import GRPCCore +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), - config: .defaults() + 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 76332bada..000000000 --- a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step04-run-client.swift +++ /dev/null @@ -1,14 +0,0 @@ -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 index e231432bc..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 @@ -1,16 +1,15 @@ -import GRPCHTTP2Transport +import GRPCCore +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), - config: .defaults() + transportSecurity: .plaintext ) - ) - - async let _ = client.run() - - let routeGuide = Routeguide_RouteGuideClient(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 5d4e5e725..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 @@ -1,21 +1,20 @@ -import GRPCHTTP2Transport +import GRPCCore +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), - config: .defaults() + transportSecurity: .plaintext ) - ) - - async let _ = client.run() - - let routeGuide = Routeguide_RouteGuideClient(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_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 d22daa343..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 @@ -1,22 +1,21 @@ -import GRPCHTTP2Transport +import GRPCCore +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), - config: .defaults() + transportSecurity: .plaintext ) - ) - - async let _ = client.run() - - let routeGuide = Routeguide_RouteGuideClient(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_RouteGuideClient) async throws { + private func getFeature(using routeGuide: Routeguide_RouteGuide.Client) async throws { print("โ†’ Calling 'GetFeature'") let point = Routeguide_Point.with { @@ -28,7 +27,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 6b5ecaba7..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 @@ -1,23 +1,22 @@ -import GRPCHTTP2Transport +import GRPCCore +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), - config: .defaults() + transportSecurity: .plaintext ) - ) - - 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) + ) { 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_RouteGuideClient) async throws { + private func getFeature(using routeGuide: Routeguide_RouteGuide.Client) async throws { print("โ†’ Calling 'GetFeature'") let point = Routeguide_Point.with { @@ -29,7 +28,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 { @@ -52,7 +51,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 a0fa49c55..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 @@ -1,24 +1,23 @@ -import GRPCHTTP2Transport +import GRPCCore +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), - config: .defaults() + transportSecurity: .plaintext ) - ) - - 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) + ) { 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_RouteGuideClient) async throws { + private func getFeature(using routeGuide: Routeguide_RouteGuide.Client) async throws { print("โ†’ Calling 'GetFeature'") let point = Routeguide_Point.with { @@ -30,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 { @@ -53,7 +52,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() @@ -75,7 +74,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 52b404049..c1d8e42b2 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/" ) @@ -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`. @@ -64,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 dependency must be on the `main` branch. + 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. @@ -77,15 +87,31 @@ 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 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. - > 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") } + + @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") + } } } @@ -95,16 +121,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 { @@ -115,7 +151,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 { @@ -125,7 +161,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 { @@ -135,7 +171,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 { @@ -148,56 +184,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`. - - > `protoc-gen-grpc-swift` is currently shared with v1 so the `_V2=true` option is required. - > This will be removed when v2 is released. + 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. } } } @@ -218,11 +228,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") } @@ -245,47 +255,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.Single` 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.Single` 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.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. + 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 `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. + 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 `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 + 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. @@ -303,11 +304,9 @@ } @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. + 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") } @@ -320,7 +319,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 +330,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 +378,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. @@ -417,20 +416,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. @@ -485,13 +479,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") } diff --git a/Sources/GRPCCore/GRPCClient.swift b/Sources/GRPCCore/GRPCClient.swift index 6c2729548..97e540ae5 100644 --- a/Sources/GRPCCore/GRPCClient.swift +++ b/Sources/GRPCCore/GRPCClient.swift @@ -21,112 +21,51 @@ 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. /// -/// 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 inProcessServerTransport = InProcessServerTransport() -/// let inProcessClientTransport = InProcessClientTransport(serverTransport: inProcessServerTransport) -/// -/// let client = GRPCClient( -/// transport: inProcessClientTransport, -/// interceptors: [StatsRecordingClientInterceptor()], -/// configuration: configuration -/// ) +/// let transport: any ClientTransport = ... +/// try await withGRPCClient(transport: transport) { client in +/// // ... +/// } /// ``` /// -/// ## 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. +/// ## Creating a client manually /// -/// ```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.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 ``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 /// 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 { +@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: 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] + private let transport: Transport /// 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`. @@ -152,7 +91,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. + """ ) } } @@ -177,7 +120,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: @@ -189,22 +132,79 @@ public final class GRPCClient: Sendable { } } + private struct StateMachine { + var state: State + + private let interceptorPipeline: [ConditionalInterceptor] + + /// 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: [ConditionalInterceptor]) { + 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( - transport: some ClientTransport, + convenience public init( + transport: Transport, 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 ``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: Transport, + interceptorPipeline: [ConditionalInterceptor] ) { self.transport = transport - self.interceptors = interceptors - self.state = Mutex(.notStarted) + self.stateMachine = Mutex(StateMachine(interceptorPipeline: interceptorPipeline)) } /// Start the client. @@ -214,12 +214,12 @@ 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() } + public func runConnections() async throws { + 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,9 +237,9 @@ public final class 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. + /// executing ``runConnections()`` 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() } @@ -253,26 +253,28 @@ 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.Single, + request: ClientRequest, descriptor: MethodDescriptor, serializer: some MessageSerializer, deserializer: some MessageDeserializer, options: CallOptions, - handler: @Sendable @escaping (ClientResponse.Single) async throws -> ReturnValue + onResponse handleResponse: @Sendable @escaping ( + _ response: 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) - return try await handler(singleResponse) + let singleResponse = await ClientResponse(stream: stream) + return try await handleResponse(singleResponse) } } @@ -284,16 +286,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: ClientRequest.Stream, + request: StreamingClientRequest, descriptor: MethodDescriptor, serializer: some MessageSerializer, deserializer: some MessageDeserializer, options: CallOptions, - handler: @Sendable @escaping (ClientResponse.Single) async throws -> ReturnValue + onResponse handleResponse: @Sendable @escaping ( + _ response: ClientResponse + ) async throws -> ReturnValue ) async throws -> ReturnValue { try await self.bidirectionalStreaming( request: request, @@ -302,8 +306,8 @@ public final class GRPCClient: Sendable { deserializer: deserializer, options: options ) { stream in - let singleResponse = await ClientResponse.Single(stream: stream) - return try await handler(singleResponse) + let singleResponse = await ClientResponse(stream: stream) + return try await handleResponse(singleResponse) } } @@ -315,30 +319,32 @@ 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.Single, + request: ClientRequest, descriptor: MethodDescriptor, serializer: some MessageSerializer, deserializer: some MessageDeserializer, options: CallOptions, - handler: @Sendable @escaping (ClientResponse.Stream) async throws -> ReturnValue + onResponse handleResponse: @Sendable @escaping ( + _ response: 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, options: options, - handler: handler + onResponse: handleResponse ) } /// 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: @@ -347,18 +353,22 @@ 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: ClientRequest.Stream, + request: StreamingClientRequest, descriptor: MethodDescriptor, serializer: some MessageSerializer, deserializer: some MessageDeserializer, options: CallOptions, - handler: @Sendable @escaping (ClientResponse.Stream) async throws -> ReturnValue + onResponse handleResponse: @Sendable @escaping ( + _ response: 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) @@ -370,8 +380,71 @@ public final class GRPCClient: Sendable { serializer: serializer, deserializer: deserializer, transport: self.transport, - interceptors: self.interceptors, - handler: handler + interceptors: applicableInterceptors, + handler: handleResponse ) } } + +/// 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. +@available(gRPCSwift 2.0, *) +@available(*, deprecated, message: "See https://forums.swift.org/t/80177") +public func withGRPCClient( + transport: Transport, + 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 ``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. +/// - 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. +@available(gRPCSwift 2.0, *) +@available(*, deprecated, message: "See https://forums.swift.org/t/80177") +public func withGRPCClient( + transport: Transport, + interceptorPipeline: [ConditionalInterceptor], + 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.runConnections() + } + + let result = try await handleClient(client) + client.beginGracefulShutdown() + return result + } +} diff --git a/Sources/GRPCCore/GRPCServer.swift b/Sources/GRPCCore/GRPCServer.swift index b3d99b7de..3aa55d3c4 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 = InProcessServerTransport() +/// // Create a transport +/// let transport = SomeServerTransport() /// /// // 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, +/// // 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. @@ -68,24 +73,18 @@ 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). -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -public final class GRPCServer: Sendable { - typealias Stream = RPCStream +/// 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 /// 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 - - /// 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] + private let router: RPCRouter /// The state of the server. private let state: Mutex @@ -140,7 +139,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. @@ -151,37 +150,50 @@ 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] = [] ) { - 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. + /// Creates a new server. /// /// - 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( - transport: any ServerTransport, - router: RPCRouter, - interceptors: [any ServerInterceptor] = [] + public convenience init( + transport: Transport, + services: [any RegistrableRPCService], + interceptorPipeline: [ConditionalInterceptor] ) { + 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 a pre-configured router. + /// + /// - 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: Transport, 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. @@ -207,7 +219,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( @@ -231,3 +243,77 @@ 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. +@available(gRPCSwift 2.0, *) +@available(*, deprecated, message: "See https://forums.swift.org/t/80177") +public func withGRPCServer( + transport: Transport, + 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. +@available(gRPCSwift 2.0, *) +@available(*, deprecated, message: "See https://forums.swift.org/t/80177") +public func withGRPCServer( + transport: Transport, + services: [any RegistrableRPCService], + interceptorPipeline: [ConditionalInterceptor], + 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/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/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/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 9bff423e3..76c7e459d 100644 --- a/Sources/GRPCCore/Internal/Metadata+GRPC.swift +++ b/Sources/GRPCCore/Internal/Metadata+GRPC.swift @@ -14,7 +14,7 @@ * limitations under the License. */ -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@available(gRPCSwift 2.0, *) extension Metadata { @inlinable var previousRPCAttempts: Int? { @@ -52,6 +52,7 @@ extension Metadata { } } +@available(gRPCSwift 2.0, *) extension Metadata { @usableFromInline enum GRPCKey: String, Sendable, Hashable { @@ -76,8 +77,8 @@ extension Metadata { } } +@available(gRPCSwift 2.0, *) extension Metadata { - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) @usableFromInline enum RetryPushback: Hashable, Sendable { case retryAfter(Duration) @@ -93,7 +94,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/Internal/MethodConfigs.swift b/Sources/GRPCCore/Internal/MethodConfigs.swift index 1992b59a8..326237def 100644 --- a/Sources/GRPCCore/Internal/MethodConfigs.swift +++ b/Sources/GRPCCore/Internal/MethodConfigs.swift @@ -22,7 +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(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@available(gRPCSwift 2.0, *) package struct MethodConfigs: Sendable, Hashable { private var elements: [MethodConfig.Name: MethodConfig] @@ -52,7 +52,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 @@ -71,7 +74,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/Internal/Result+Catching.swift b/Sources/GRPCCore/Internal/Result+Catching.swift index 68bbbebd7..07258ca58 100644 --- a/Sources/GRPCCore/Internal/Result+Catching.swift +++ b/Sources/GRPCCore/Internal/Result+Catching.swift @@ -14,13 +14,13 @@ * limitations under the License. */ -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -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 { + @available(gRPCSwift 2.0, *) + init(catching body: () async throws(Failure) -> Success) async { do { self = .success(try await body()) } catch { @@ -36,6 +36,7 @@ extension Result where Failure == any Error { /// - 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 454a85b85..4b6fd5ca8 100644 --- a/Sources/GRPCCore/Internal/TaskGroup+CancellableTask.swift +++ b/Sources/GRPCCore/Internal/TaskGroup+CancellableTask.swift @@ -14,7 +14,7 @@ * limitations under the License. */ -@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +@available(gRPCSwift 2.0, *) extension TaskGroup { /// Adds a child task to the group which is individually cancellable. /// @@ -64,8 +64,8 @@ extension TaskGroup { } } +@available(gRPCSwift 2.0, *) @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/Metadata.swift b/Sources/GRPCCore/Metadata.swift index 8326eb336..b3ea93810 100644 --- a/Sources/GRPCCore/Metadata.swift +++ b/Sources/GRPCCore/Metadata.swift @@ -79,6 +79,8 @@ /// - 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, *) +@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. @@ -171,6 +173,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. @@ -248,6 +264,7 @@ public struct Metadata: Sendable, Hashable { } } +@available(gRPCSwift 2.0, *) extension Metadata: RandomAccessCollection { public typealias Element = (key: String, value: Value) @@ -288,6 +305,7 @@ extension Metadata: RandomAccessCollection { } } +@available(gRPCSwift 2.0, *) extension Metadata { /// A sequence of metadata values for a given key. public struct Values: Sequence, Sendable { @@ -335,8 +353,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. @@ -385,6 +403,7 @@ extension Metadata { } } +@available(gRPCSwift 2.0, *) extension Metadata { /// A sequence of metadata binary values for a given key. public struct BinaryValues: Sequence, Sendable { @@ -405,7 +424,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 } @@ -446,30 +465,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 @@ -478,12 +502,20 @@ extension Metadata.Value: ExpressibleByArrayLiteral { } } +@available(gRPCSwift 2.0, *) 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)]" + } } } +@available(gRPCSwift 2.0, *) extension Metadata.Value: CustomStringConvertible { public var description: String { switch self { @@ -494,3 +526,15 @@ extension Metadata.Value: CustomStringConvertible { } } } + +@available(gRPCSwift 2.0, *) +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/Sources/GRPCCore/MethodDescriptor.swift b/Sources/GRPCCore/MethodDescriptor.swift index 8d2795ac1..a677ff982 100644 --- a/Sources/GRPCCore/MethodDescriptor.swift +++ b/Sources/GRPCCore/MethodDescriptor.swift @@ -15,12 +15,10 @@ */ /// A description of a method on a service. +@available(gRPCSwift 2.0, *) 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 +37,26 @@ 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(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 7354a7b83..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 @@ -35,18 +36,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``. @@ -72,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 { @@ -82,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. @@ -129,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) @@ -233,3 +281,64 @@ 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. +@available(gRPCSwift 2.0, *) +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 } +} + +@available(gRPCSwift 2.0, *) +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 + } +} + +@available(gRPCSwift 2.0, *) +extension RPCErrorConvertible where Self: Error { + /// The original error which led to this error being thrown. + public var rpcErrorCause: (any Error)? { + self + } +} + +@available(gRPCSwift 2.0, *) +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/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 b09730c3b..a8b7d3abd 100644 --- a/Sources/GRPCCore/ServiceDescriptor.swift +++ b/Sources/GRPCCore/ServiceDescriptor.swift @@ -15,23 +15,37 @@ */ /// 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. - public var package: String + public var package: String { + if let index = self.fullyQualifiedService.utf8.lastIndex(of: UInt8(ascii: ".")) { + return String(self.fullyQualifiedService[..: 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..afd8e6661 100644 --- a/Sources/GRPCCore/Streaming/Internal/BroadcastAsyncSequence+RPCWriter.swift +++ b/Sources/GRPCCore/Streaming/Internal/BroadcastAsyncSequence+RPCWriter.swift @@ -14,7 +14,7 @@ * limitations under the License. */ -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +@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 5f812eb11..c40e01585 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. /// @@ -30,8 +31,8 @@ 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 +@available(gRPCSwift 2.0, *) struct BroadcastAsyncSequence: Sendable, AsyncSequence { @usableFromInline let _storage: _BroadcastSequenceStorage @@ -85,7 +86,7 @@ struct BroadcastAsyncSequence: Sendable, AsyncSequence { // MARK: - AsyncIterator -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +@available(gRPCSwift 2.0, *) extension BroadcastAsyncSequence { @usableFromInline struct AsyncIterator: AsyncIteratorProtocol { @@ -112,7 +113,7 @@ extension BroadcastAsyncSequence { // MARK: - Continuation -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +@available(gRPCSwift 2.0, *) extension BroadcastAsyncSequence { @usableFromInline struct Source: Sendable { @@ -147,6 +148,7 @@ extension BroadcastAsyncSequence { } @usableFromInline +@available(gRPCSwift 2.0, *) enum BroadcastAsyncSequenceError: Error { /// The consumer was too slow. case consumingTooSlow @@ -156,19 +158,19 @@ enum BroadcastAsyncSequenceError: Error { // MARK: - Storage -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) @usableFromInline +@available(gRPCSwift 2.0, *) 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() } @@ -188,7 +190,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: @@ -200,7 +202,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) } @@ -212,7 +214,7 @@ final class _BroadcastSequenceStorage: Sendable { } } } onCancel: { - let onCancel = self._state.withLockedValue { state in + let onCancel = self._state.withLock { state in state.cancelProducer(withToken: token) } @@ -234,7 +236,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: () @@ -251,7 +253,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. @@ -263,35 +265,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) } @@ -308,7 +307,7 @@ final class _BroadcastSequenceStorage: Sendable { /// elements. @inlinable var isKnownSafeForNextSubscriber: Bool { - self._state.withLockedValue { state in + self._state.withLock { state in state.nextSubscriptionIsValid } } @@ -316,7 +315,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() } @@ -331,8 +330,8 @@ final class _BroadcastSequenceStorage: Sendable { // MARK: - State machine -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) @usableFromInline +@available(gRPCSwift 2.0, *) struct _BroadcastSequenceStateMachine: Sendable { @usableFromInline typealias ConsumerContinuation = CheckedContinuation @@ -472,10 +471,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) + } } } @@ -702,10 +708,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) + } } } @@ -1154,7 +1167,7 @@ struct _BroadcastSequenceStateMachine: Sendable { @usableFromInline enum OnSetContinuation { case none - case resume(ConsumerContinuation, Result) + case resume(ConsumerContinuation, Result, ProducerContinuations?) } @inlinable @@ -1180,7 +1193,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. @@ -1369,7 +1382,7 @@ struct _BroadcastSequenceStateMachine: Sendable { } } -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +@available(gRPCSwift 2.0, *) extension _BroadcastSequenceStateMachine { /// A collection of elements tagged with an identifier. /// @@ -1524,7 +1537,7 @@ extension _BroadcastSequenceStateMachine { } } -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +@available(gRPCSwift 2.0, *) extension _BroadcastSequenceStateMachine { /// A collection of subscriptions. @usableFromInline @@ -1788,7 +1801,7 @@ extension _BroadcastSequenceStateMachine { } } -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +@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 5b4bcb58b..f0ff95878 100644 --- a/Sources/GRPCCore/Streaming/Internal/GRPCAsyncThrowingStream.swift +++ b/Sources/GRPCCore/Streaming/Internal/GRPCAsyncThrowingStream.swift @@ -19,7 +19,7 @@ // '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, *) +@available(gRPCSwift 2.0, *) package struct GRPCAsyncThrowingStream: AsyncSequence, Sendable { package typealias Element = Element package typealias Failure = any Error @@ -78,7 +78,7 @@ package struct GRPCAsyncThrowingStream: AsyncSequence, Sendab } } -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +@available(gRPCSwift 2.0, *) extension GRPCAsyncThrowingStream.Continuation: RPCWriterProtocol { package func write(_ element: Element) async throws { self.yield(element) @@ -91,7 +91,7 @@ extension GRPCAsyncThrowingStream.Continuation: RPCWriterProtocol { } } -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +@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 7755b46e1..d5349d2bf 100644 --- a/Sources/GRPCCore/Streaming/Internal/RPCWriter+Map.swift +++ b/Sources/GRPCCore/Streaming/Internal/RPCWriter+Map.swift @@ -14,8 +14,8 @@ * limitations under the License. */ -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) @usableFromInline +@available(gRPCSwift 2.0, *) struct MapRPCWriter< Value: Sendable, Mapped: Sendable, @@ -47,7 +47,7 @@ struct MapRPCWriter< } } -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +@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 c3dc290e9..c1a0a9b51 100644 --- a/Sources/GRPCCore/Streaming/Internal/RPCWriter+MessageToRPCResponsePart.swift +++ b/Sources/GRPCCore/Streaming/Internal/RPCWriter+MessageToRPCResponsePart.swift @@ -14,21 +14,22 @@ * limitations under the License. */ -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) @usableFromInline +@available(gRPCSwift 2.0, *) 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) } @@ -40,7 +41,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)) } @@ -48,11 +49,11 @@ struct MessageToRPCResponsePartWriter< } } -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +@available(gRPCSwift 2.0, *) 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 f2a9acd22..b2c714fee 100644 --- a/Sources/GRPCCore/Streaming/Internal/RPCWriter+Serialize.swift +++ b/Sources/GRPCCore/Streaming/Internal/RPCWriter+Serialize.swift @@ -14,10 +14,11 @@ * limitations under the License. */ -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) @usableFromInline +@available(gRPCSwift 2.0, *) struct SerializingRPCWriter< - Base: RPCWriterProtocol<[UInt8]>, + Base: RPCWriterProtocol, + Bytes: GRPCContiguousBytes, Serializer: MessageSerializer >: RPCWriterProtocol where Serializer.Message: Sendable { @usableFromInline @@ -42,14 +43,14 @@ 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) } } -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +@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 e3bdcafee..4beae5841 100644 --- a/Sources/GRPCCore/Streaming/Internal/UncheckedAsyncIteratorSequence.swift +++ b/Sources/GRPCCore/Streaming/Internal/UncheckedAsyncIteratorSequence.swift @@ -16,9 +16,9 @@ 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. +@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 d3603fe00..de44eb5aa 100644 --- a/Sources/GRPCCore/Streaming/RPCAsyncSequence.swift +++ b/Sources/GRPCCore/Streaming/RPCAsyncSequence.swift @@ -15,7 +15,7 @@ */ /// A type-erasing `AsyncSequence`. -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +@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 dda689464..51d2824d1 100644 --- a/Sources/GRPCCore/Streaming/RPCWriter+Closable.swift +++ b/Sources/GRPCCore/Streaming/RPCWriter+Closable.swift @@ -14,7 +14,7 @@ * limitations under the License. */ -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +@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 d0b7b940b..866aa974e 100644 --- a/Sources/GRPCCore/Streaming/RPCWriter.swift +++ b/Sources/GRPCCore/Streaming/RPCWriter.swift @@ -15,7 +15,7 @@ */ /// A type-erasing ``RPCWriterProtocol``. -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +@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 63e3ee26d..bde8369ec 100644 --- a/Sources/GRPCCore/Streaming/RPCWriterProtocol.swift +++ b/Sources/GRPCCore/Streaming/RPCWriterProtocol.swift @@ -14,8 +14,8 @@ * limitations under the License. */ -/// A sink for values which are produced over time. -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +/// 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 @@ -37,7 +37,7 @@ public protocol RPCWriterProtocol: Sendable { func write(contentsOf elements: some Sequence) async throws } -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +@available(gRPCSwift 2.0, *) extension RPCWriterProtocol { /// Writes an `AsyncSequence` of values into the sink. /// @@ -51,7 +51,8 @@ extension RPCWriterProtocol { } } -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +/// 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 4640a5cca..c7150b99c 100644 --- a/Sources/GRPCCore/Timeout.swift +++ b/Sources/GRPCCore/Timeout.swift @@ -22,8 +22,8 @@ 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 +@available(gRPCSwift 2.0, *) struct Timeout: CustomStringConvertible, Hashable, Sendable { /// Possible units for a ``Timeout``. internal enum Unit: Character { @@ -47,7 +47,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)" } @@ -182,7 +182,7 @@ extension Int64 { } } -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@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 800aa5b64..89d21e1c3 100644 --- a/Sources/GRPCCore/Transport/ClientTransport.swift +++ b/Sources/GRPCCore/Transport/ClientTransport.swift @@ -14,10 +14,25 @@ * 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 +/// 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. +@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 + + typealias Inbound = RPCAsyncSequence, any Error> + typealias Outbound = RPCWriter>.Closable /// Returns a throttle which gRPC uses to determine whether retries can be executed. /// @@ -48,7 +63,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. /// @@ -60,12 +75,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/GRPCCore/Transport/RPCParts.swift b/Sources/GRPCCore/Transport/RPCParts.swift index cd72f7efb..8ebf12787 100644 --- a/Sources/GRPCCore/Transport/RPCParts.swift +++ b/Sources/GRPCCore/Transport/RPCParts.swift @@ -15,7 +15,8 @@ */ /// Part of a request sent from a client to a server in a stream. -public enum RPCRequestPart: Hashable, Sendable { +@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. case metadata(Metadata) @@ -23,11 +24,19 @@ 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) } +@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. -public enum RPCResponsePart: Hashable, Sendable { +@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 /// the response stream. @@ -36,10 +45,17 @@ 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) } + +@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 eca345c2a..0c976265f 100644 --- a/Sources/GRPCCore/Transport/RPCStream.swift +++ b/Sources/GRPCCore/Transport/RPCStream.swift @@ -15,7 +15,7 @@ */ /// A bidirectional communication channel between a client and server for a given method. -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +@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 70c934dfc..7c6b05baa 100644 --- a/Sources/GRPCCore/Transport/RetryThrottle.swift +++ b/Sources/GRPCCore/Transport/RetryThrottle.swift @@ -29,8 +29,8 @@ 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). -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +/// 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 79c5c0fc6..62488a3bb 100644 --- a/Sources/GRPCCore/Transport/ServerTransport.swift +++ b/Sources/GRPCCore/Transport/ServerTransport.swift @@ -14,11 +14,22 @@ * limitations under the License. */ -/// 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 +/// 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. +@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 + + typealias Inbound = RPCAsyncSequence, any Error> + typealias Outbound = RPCWriter>.Closable /// Starts the transport. /// 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/GRPCInProcessTransport/Documentation.docc/Documentation.md b/Sources/GRPCInProcessTransport/Documentation.docc/Documentation.md new file mode 100644 index 000000000..19d0ab978 --- /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`` +- ``InProcessTransport/Client`` +- ``InProcessTransport/Server`` 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 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..703b9f62c --- /dev/null +++ b/Sources/GRPCInProcessTransport/InProcessTransport+Client.swift @@ -0,0 +1,369 @@ +/* + * Copyright 2024, gRPC Authors All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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(gRPCSwift 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` + public final class Client: ClientTransport { + public typealias Bytes = [UInt8] + + 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, any Error>, + 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, any Error>, + 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 let retryThrottle: RetryThrottle? + + 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(), + 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. + /// + /// 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 and the client context as its parameters. + /// - Returns: Whatever value was returned from `closure`. + public func withStream( + descriptor: MethodDescriptor, + 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 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) + } + } + + 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, clientContext) + 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..cc01e2046 --- /dev/null +++ b/Sources/GRPCInProcessTransport/InProcessTransport+Server.swift @@ -0,0 +1,149 @@ +/* + * Copyright 2024, gRPC Authors All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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(gRPCSwift 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(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()``. + /// + /// - SeeAlso: `ClientTransport` + public final class Server: ServerTransport, Sendable { + public typealias Bytes = [UInt8] + + public typealias Inbound = RPCAsyncSequence, any Error> + public typealias Outbound = RPCWriter>.Closable + + private let newStreams: AsyncStream> + private let newStreamsContinuation: AsyncStream>.Continuation + package let peer: String + + 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``. + /// + /// - 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()) + self.peer = peer + } + + /// 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 { + 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, + remotePeer: self.peer, + localPeer: self.peer, + cancellation: handle + ) + await streamHandler(stream, context) + } + } + } + } + } + + /// Stop listening to any new `RPCStream` publications. + /// + /// - SeeAlso: `ServerTransport` + public func beginGracefulShutdown() { + self.newStreamsContinuation.finish() + for handle in self.handles.withLock({ $0.beginShutdown() }) { + handle.cancel() + } + } + } +} diff --git a/Sources/GRPCInProcessTransport/InProcessTransport.swift b/Sources/GRPCInProcessTransport/InProcessTransport.swift index 32a2002e9..3ceab8156 100644 --- a/Sources/GRPCInProcessTransport/InProcessTransport.swift +++ b/Sources/GRPCInProcessTransport/InProcessTransport.swift @@ -16,24 +16,19 @@ 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(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 + + /// 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()) { + let peer = "in-process:\(System.pid())" + self.server = Self.Server(peer: peer) + self.client = Self.Client(server: self.server, serviceConfig: serviceConfig, peer: peer) } } diff --git a/Sources/GRPCInProcessTransport/Syscalls.swift b/Sources/GRPCInProcessTransport/Syscalls.swift new file mode 100644 index 000000000..c96becbd5 --- /dev/null +++ b/Sources/GRPCInProcessTransport/Syscalls.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. + */ + +#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) +private import Musl // should be @usableFromInline +#else +#error("Unsupported OS") +#endif + +enum System { + static func pid() -> Int { + #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) + #elseif canImport(Musl) + let pid = Musl.getpid() + return Int(pid) + #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 2d9708523..000000000 Binary files a/Sources/GRPCSampleData/bundle.p12 and /dev/null differ 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/GRPCCodeGenTests/Internal/Renderer/TextBasedRendererTests.swift b/Tests/GRPCCodeGenTests/Internal/Renderer/TextBasedRendererTests.swift index 65b5eb79b..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( @@ -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 { @@ -979,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 new file mode 100644 index 000000000..4e697a87e --- /dev/null +++ b/Tests/GRPCCodeGenTests/Internal/StructuredSwift+ClientTests.swift @@ -0,0 +1,651 @@ +/* + * Copyright 2024, gRPC Authors All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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 StructuredSwiftTests { + @Suite("Client") + struct Client { + @Test( + "func (request:serializer:deserializer:options:onResponse:)", + arguments: AccessModifier.allCases, + RPCKind.allCases + ) + @available(gRPCSwift 2.0, *) + 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] + ) + @available(gRPCSwift 2.0, *) + 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) + @available(gRPCSwift 2.0, *) + func clientProtocol(access: AccessModifier) { + let decl: ProtocolDescription = .clientProtocol( + accessLevel: access, + name: "Foo_ClientProtocol", + methods: [ + .init( + documentation: "/// Some docs", + name: MethodName(identifyingName: "Bar", typeName: "Bar", functionName: "bar"), + isInputStreaming: false, + isOutputStreaming: false, + inputType: "BarInput", + outputType: "BarOutput" + ) + ] + ) + + let expected = """ + \(access) protocol Foo_ClientProtocol: Sendable { + /// 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, + 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) + @available(gRPCSwift 2.0, *) + 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 + ) + @available(gRPCSwift 2.0, *) + func extensionWithDefaultClientMethods(access: AccessModifier) { + let decl: ExtensionDescription = .clientMethodSignatureWithDefaults( + accessLevel: access, + name: "Foo_ClientProtocol", + methods: [ + MethodDescriptor( + documentation: "", + name: MethodName(identifyingName: "Bar", typeName: "Bar", functionName: "bar"), + isInputStreaming: false, + isOutputStreaming: false, + inputType: "BarInput", + outputType: "BarOutput" + ) + ], + serializer: { "Serialize<\($0)>()" }, + deserializer: { "Deserialize<\($0)>()" } + ) + + 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, + 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 + ) + @available(gRPCSwift 2.0, *) + 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] + ) + @available(gRPCSwift 2.0, *) + 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) + @available(gRPCSwift 2.0, *) + func explodedClientMethodExtension(access: AccessModifier) { + let decl: ExtensionDescription = .explodedClientMethods( + accessLevel: access, + on: "Foo_ClientProtocol", + methods: [ + .init( + documentation: "/// Some docs", + name: MethodName(identifyingName: "Bar", typeName: "Bar", functionName: "bar"), + isInputStreaming: false, + isOutputStreaming: true, + inputType: "Input", + outputType: "Output" + ) + ] + ) + + let expected = """ + extension Foo_ClientProtocol { + /// 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 = [:], + 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 + ) + @available(gRPCSwift 2.0, *) + 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) + @available(gRPCSwift 2.0, *) + func client(access: AccessModifier) { + let decl: StructDescription = .client( + accessLevel: access, + name: "FooClient", + serviceEnum: "BarService", + clientProtocol: "Foo_ClientProtocol", + methods: [ + .init( + documentation: "/// Unary docs", + name: MethodName( + identifyingName: "Unary", + typeName: "Unary", + functionName: "unary" + ), + isInputStreaming: false, + isOutputStreaming: false, + inputType: "Input", + outputType: "Output" + ), + .init( + documentation: "/// ClientStreaming docs", + name: MethodName( + identifyingName: "ClientStreaming", + typeName: "ClientStreaming", + functionName: "clientStreaming" + ), + + isInputStreaming: true, + isOutputStreaming: false, + inputType: "Input", + outputType: "Output" + ), + .init( + documentation: "/// ServerStreaming docs", + name: MethodName( + identifyingName: "ServerStreaming", + typeName: "ServerStreaming", + functionName: "serverStreaming" + ), + isInputStreaming: false, + isOutputStreaming: true, + inputType: "Input", + outputType: "Output" + ), + .init( + documentation: "/// BidiStreaming docs", + name: MethodName( + identifyingName: "BidiStreaming", + typeName: "BidiStreaming", + functionName: "bidiStreaming" + ), + isInputStreaming: true, + isOutputStreaming: true, + inputType: "Input", + outputType: "Output" + ), + ] + ) + + let expected = """ + \(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) { + self.client = client + } + + /// 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, + 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 + ) + } + + /// 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, + 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 + ) + } + + /// 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, + 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 + ) + } + + /// 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, + 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) + } + } +} diff --git a/Tests/GRPCCodeGenTests/Internal/StructuredSwift+ImportTests.swift b/Tests/GRPCCodeGenTests/Internal/StructuredSwift+ImportTests.swift new file mode 100644 index 000000000..fdb8b6813 --- /dev/null +++ b/Tests/GRPCCodeGenTests/Internal/StructuredSwift+ImportTests.swift @@ -0,0 +1,227 @@ +/* + * 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 { + @available(gRPCSwift 2.0, *) + static let translator = IDLToStructuredSwiftTranslator() + + @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)) + 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.translator.makeImports( + dependencies: dependencies, + accessLevel: accessLevel, + accessLevelOnImports: true, + grpcCoreModuleName: "GRPCCore" + ) + + #expect(render(imports) == expected) + } + + @Test( + "preconcurrency import rendering", + arguments: StructuredSwiftTests.allAccessLevels + ) + @available(gRPCSwift 2.0, *) + func preconcurrencyImports(accessLevel: CodeGenerator.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.translator.makeImports( + dependencies: dependencies, + accessLevel: accessLevel, + accessLevelOnImports: true, + grpcCoreModuleName: "GRPCCore" + ) + + #expect(render(imports) == expected) + } + + @Test( + "SPI import rendering", + arguments: StructuredSwiftTests.allAccessLevels + ) + @available(gRPCSwift 2.0, *) + func spiImports(accessLevel: CodeGenerator.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.translator.makeImports( + dependencies: dependencies, + accessLevel: accessLevel, + accessLevelOnImports: true, + grpcCoreModuleName: "GRPCCore" + ) + + #expect(render(imports) == expected) + } + + @Test("gRPC module name") + @available(gRPCSwift 2.0, *) + 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/StructuredSwift+MetadataTests.swift b/Tests/GRPCCodeGenTests/Internal/StructuredSwift+MetadataTests.swift new file mode 100644 index 000000000..1f297e66f --- /dev/null +++ b/Tests/GRPCCodeGenTests/Internal/StructuredSwift+MetadataTests.swift @@ -0,0 +1,276 @@ +/* + * Copyright 2024, gRPC Authors All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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 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" + #expect(render(.typealias(decl)) == expected) + } + + @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" + #expect(render(.typealias(decl)) == expected) + } + + @Test( + "static let descriptor = GRPCCore.MethodDescriptor(...)", + arguments: AccessModifier.allCases + ) + @available(gRPCSwift 2.0, *) + func staticMethodDescriptorProperty(access: AccessModifier) { + let decl: VariableDescription = .methodDescriptor( + accessModifier: access, + literalFullyQualifiedService: "foo.Foo", + literalMethodName: "Bar" + ) + + let expected = """ + \(access) static let descriptor = GRPCCore.MethodDescriptor( + service: GRPCCore.ServiceDescriptor(fullyQualifiedService: "foo.Foo"), + method: "Bar" + ) + """ + #expect(render(.variable(decl)) == expected) + } + + @Test( + "static let descriptor = GRPCCore.ServiceDescriptor(fullyQualifiedService:)", + arguments: AccessModifier.allCases + ) + @available(gRPCSwift 2.0, *) + func staticServiceDescriptorProperty(access: AccessModifier) { + let decl: VariableDescription = .serviceDescriptor( + accessModifier: access, + literalFullyQualifiedService: "foo.Bar" + ) + + let expected = """ + \(access) static let descriptor = GRPCCore.ServiceDescriptor(fullyQualifiedService: "foo.Bar") + """ + #expect(render(.variable(decl)) == expected) + } + + @Test("extension GRPCCore.ServiceDescriptor { ... }", arguments: AccessModifier.allCases) + @available(gRPCSwift 2.0, *) + func staticServiceDescriptorPropertyExtension(access: AccessModifier) { + let decl: ExtensionDescription = .serviceDescriptor( + accessModifier: access, + propertyName: "foo", + literalFullyQualifiedService: "echo.EchoService" + ) + + let expected = """ + extension GRPCCore.ServiceDescriptor { + /// Service descriptor for the "echo.EchoService" service. + \(access) static let foo = GRPCCore.ServiceDescriptor(fullyQualifiedService: "echo.EchoService") + } + """ + #expect(render(.extension(decl)) == expected) + } + + @Test( + "static let descriptors: [GRPCCore.MethodDescriptor] = [...]", + arguments: AccessModifier.allCases + ) + @available(gRPCSwift 2.0, *) + 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) + @available(gRPCSwift 2.0, *) + func methodNamespaceEnum(access: AccessModifier) { + let decl: EnumDescription = .methodNamespace( + accessModifier: access, + name: "Foo", + literalMethod: "Foo", + literalFullyQualifiedService: "bar.Bar", + inputType: "FooInput", + outputType: "FooOutput" + ) + + 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" + ) + } + """ + #expect(render(.enum(decl)) == expected) + } + + @Test("enum Method { ... }", arguments: AccessModifier.allCases) + @available(gRPCSwift 2.0, *) + func methodsNamespaceEnum(access: AccessModifier) { + let decl: EnumDescription = .methodsNamespace( + accessModifier: access, + literalFullyQualifiedService: "bar.Bar", + methods: [ + .init( + documentation: "", + name: MethodName(identifyingName: "Foo", typeName: "Foo", functionName: "foo"), + isInputStreaming: false, + isOutputStreaming: false, + inputType: "FooInput", + outputType: "FooOutput" + ) + ] + ) + + 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 + ] + } + """ + #expect(render(.enum(decl)) == expected) + } + + @Test("enum Method { ... } (no methods)", arguments: AccessModifier.allCases) + @available(gRPCSwift 2.0, *) + func methodsNamespaceEnumNoMethods(access: AccessModifier) { + let decl: EnumDescription = .methodsNamespace( + accessModifier: access, + literalFullyQualifiedService: "bar.Bar", + methods: [] + ) + + let expected = """ + \(access) enum Method { + /// Descriptors for all methods in the "bar.Bar" service. + \(access) static let descriptors: [GRPCCore.MethodDescriptor] = [] + } + """ + #expect(render(.enum(decl)) == expected) + } + + @Test("enum { ... }", arguments: AccessModifier.allCases) + @available(gRPCSwift 2.0, *) + func serviceNamespaceEnum(access: AccessModifier) { + let decl: EnumDescription = .serviceNamespace( + accessModifier: access, + name: "Foo", + literalFullyQualifiedService: "Foo", + methods: [ + .init( + documentation: "", + name: MethodName(identifyingName: "Bar", typeName: "Bar", functionName: "bar"), + isInputStreaming: false, + isOutputStreaming: false, + inputType: "BarInput", + outputType: "BarOutput" + ) + ] + ) + + 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 + ] + } + } + """ + #expect(render(.enum(decl)) == expected) + } + + @Test("enum { ... } (no methods)", arguments: AccessModifier.allCases) + @available(gRPCSwift 2.0, *) + func serviceNamespaceEnumNoMethods(access: AccessModifier) { + let decl: EnumDescription = .serviceNamespace( + accessModifier: access, + name: "Foo", + literalFullyQualifiedService: "Foo", + methods: [] + ) + + 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] = [] + } + } + """ + + #expect(render(.enum(decl)) == expected) + } + } +} diff --git a/Tests/GRPCCodeGenTests/Internal/StructuredSwift+ServerTests.swift b/Tests/GRPCCodeGenTests/Internal/StructuredSwift+ServerTests.swift new file mode 100644 index 000000000..50bfed96f --- /dev/null +++ b/Tests/GRPCCodeGenTests/Internal/StructuredSwift+ServerTests.swift @@ -0,0 +1,464 @@ +/* + * Copyright 2024, gRPC Authors All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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 StructuredSwiftTests { + @Suite("Server") + struct Server { + @Test( + "func (request:context:) async throws -> ...", + arguments: AccessModifier.allCases, + RPCKind.allCases + ) + @available(gRPCSwift 2.0, *) + 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) + @available(gRPCSwift 2.0, *) + func serverStreamingServiceProtocol(access: AccessModifier) { + let decl: ProtocolDescription = .streamingService( + accessLevel: access, + name: "FooService", + methods: [ + .init( + documentation: "/// Some docs", + name: MethodName(identifyingName: "Foo", typeName: "Foo", functionName: "foo"), + isInputStreaming: false, + isOutputStreaming: false, + inputType: "FooInput", + outputType: "FooOutput" + ) + ] + ) + + let expected = """ + \(access) protocol FooService: GRPCCore.RegistrableRPCService { + /// 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 + ) async throws -> GRPCCore.StreamingServerResponse + } + """ + + #expect(render(.protocol(decl)) == expected) + } + + @Test("protocol ServiceProtocol { ... }", arguments: AccessModifier.allCases) + @available(gRPCSwift 2.0, *) + func serverServiceProtocol(access: AccessModifier) { + let decl: ProtocolDescription = .service( + accessLevel: access, + name: "FooService", + streamingProtocol: "FooService_StreamingServiceProtocol", + methods: [ + .init( + documentation: "/// Some docs", + name: MethodName(identifyingName: "Foo", typeName: "Foo", functionName: "foo"), + isInputStreaming: false, + isOutputStreaming: false, + inputType: "FooInput", + outputType: "FooOutput" + ) + ] + ) + + let expected = """ + \(access) protocol FooService: FooService_StreamingServiceProtocol { + /// 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 + ) async throws -> GRPCCore.ServerResponse + } + """ + + #expect(render(.protocol(decl)) == expected) + } + + @Test("{ router, context in try await self.(...) }") + @available(gRPCSwift 2.0, *) + 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(...) { ... }") + @available(gRPCSwift 2.0, *) + 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) + @available(gRPCSwift 2.0, *) + func registerMethods(access: AccessModifier) { + let expression: FunctionDescription = .registerMethods( + accessLevel: access, + serviceNamespace: "FooService", + methods: [ + .init( + documentation: "", + name: MethodName(identifyingName: "Bar", typeName: "Bar", functionName: "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) where Transport: GRPCCore.ServerTransport { + 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 + ) + @available(gRPCSwift 2.0, *) + 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) + @available(gRPCSwift 2.0, *) + func streamingServiceProtocolDefaultImplementation(access: AccessModifier) { + let decl: ExtensionDescription = .streamingServiceProtocolDefaultImplementation( + accessModifier: access, + on: "Foo_ServiceProtocol", + methods: [ + .init( + documentation: "", + name: MethodName(identifyingName: "Foo", typeName: "Foo", functionName: "foo"), + isInputStreaming: false, + isOutputStreaming: false, + inputType: "FooInput", + outputType: "FooOutput" + ), + // Will be ignored as a bidirectional streaming method. + .init( + documentation: "", + name: MethodName(identifyingName: "Bar", typeName: "Bar", functionName: "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) + } + + @Test( + "func (request:response:context:) (simple)", + arguments: AccessModifier.allCases, + RPCKind.allCases + ) + @available(gRPCSwift 2.0, *) + 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) + @available(gRPCSwift 2.0, *) + func simpleServiceProtocol(access: AccessModifier) { + let decl: ProtocolDescription = .simpleServiceProtocol( + accessModifier: access, + name: "SimpleServiceProtocol", + serviceProtocol: "ServiceProtocol", + methods: [ + .init( + documentation: "", + name: MethodName(identifyingName: "Foo", typeName: "Foo", functionName: "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/StructuredSwiftTestHelpers.swift b/Tests/GRPCCodeGenTests/Internal/StructuredSwiftTestHelpers.swift new file mode 100644 index 000000000..5146aaaa1 --- /dev/null +++ b/Tests/GRPCCodeGenTests/Internal/StructuredSwiftTestHelpers.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. + */ + +import Testing + +@testable import GRPCCodeGen + +// Used as a namespace for organising other structured swift tests. +@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) + 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 + } + } +} diff --git a/Tests/GRPCCodeGenTests/Internal/Translator/ClientCodeTranslatorSnippetBasedTests.swift b/Tests/GRPCCodeGenTests/Internal/Translator/ClientCodeTranslatorSnippetBasedTests.swift index 88a713679..5a449e29e 100644 --- a/Tests/GRPCCodeGenTests/Internal/Translator/ClientCodeTranslatorSnippetBasedTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/Translator/ClientCodeTranslatorSnippetBasedTests.swift @@ -14,800 +14,216 @@ * 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 { - typealias MethodDescriptor = GRPCCodeGen.CodeGenerationRequest.ServiceDescriptor.MethodDescriptor - typealias ServiceDescriptor = GRPCCodeGen.CodeGenerationRequest.ServiceDescriptor - typealias Name = GRPCCodeGen.CodeGenerationRequest.Name - - func testClientCodeTranslatorUnaryMethod() throws { +@Suite +struct ClientCodeTranslatorSnippetBasedTests { + @Test + @available(gRPCSwift 2.0, *) + 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", outputType: "NamespaceA_ServiceAResponse" ) + 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] ) - let expectedSwift = - """ - /// Documentation for ServiceA - @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( - 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: 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: 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 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 { - private let client: GRPCCore.GRPCClient - - public init(wrapping client: GRPCCore.GRPCClient) { - self.client = client - } - - /// Documentation for MethodA - public func methodA( - 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: NamespaceA_ServiceA.Method.MethodA.descriptor, - serializer: serializer, - deserializer: deserializer, - options: options, - handler: body - ) - } - } - """ - try self.assertClientCodeTranslation( - codeGenerationRequest: makeCodeGenerationRequest(services: [service]), - expectedSwift: expectedSwift, - accessLevel: .public - ) - } + 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. + /// + /// You don't need to implement this protocol directly, use the generated + /// implementation, ``Client``. + /// + /// > Source IDL Documentation: + /// > + /// > Documentation for ServiceA + public protocol ClientProtocol: Sendable { + /// 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, + 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 "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 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) { + self.client = client + } - 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_ServiceAClientProtocol: Sendable { - /// Documentation for MethodA - func methodA( - 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: GRPCCore.ClientRequest.Stream, - options: GRPCCore.CallOptions = .defaults, - _ body: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> R = { - try $0.message + /// 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, + 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 + ) } - ) async throws -> R where R: Sendable { - try await self.methodA( - request: request, - serializer: GRPCProtobuf.ProtobufSerializer(), - deserializer: GRPCProtobuf.ProtobufDeserializer(), - options: options, - body - ) } } + // 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 { - /// Documentation for MethodA + /// 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( - metadata: GRPCCore.Metadata = [:], + request: GRPCCore.ClientRequest, 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 + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.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 { - private let client: GRPCCore.GRPCClient - - public init(wrapping client: GRPCCore.GRPCClient) { - self.client = client - } - - /// Documentation for MethodA - public func methodA( - 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: NamespaceA_ServiceA.Method.MethodA.descriptor, - serializer: serializer, - deserializer: deserializer, - options: options, - handler: body - ) - } - } - """ - - try self.assertClientCodeTranslation( - codeGenerationRequest: makeCodeGenerationRequest(services: [service]), - expectedSwift: expectedSwift, - accessLevel: .public - ) - } - - 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_ServiceAClientProtocol: Sendable { - /// Documentation for MethodA - func methodA( - 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: 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: GRPCProtobuf.ProtobufSerializer(), deserializer: GRPCProtobuf.ProtobufDeserializer(), options: options, - body + onResponse: handleResponse ) } } + // 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 { - /// 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 = [:], 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 { - private let client: GRPCCore.GRPCClient - - public init(wrapping client: GRPCCore.GRPCClient) { - self.client = client - } - - /// Documentation for MethodA - public func methodA( - 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: NamespaceA_ServiceA.Method.MethodA.descriptor, - serializer: serializer, - deserializer: deserializer, - options: options, - handler: body - ) - } - } - """ - - 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_ServiceAClientProtocol: Sendable { - /// Documentation for MethodA - func methodA( - 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: 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: 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 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 { - private let client: GRPCCore.GRPCClient - - public init(wrapping client: GRPCCore.GRPCClient) { - self.client = client - } - - /// Documentation for MethodA - public func methodA( - 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: NamespaceA_ServiceA.Method.MethodA.descriptor, - serializer: serializer, - deserializer: deserializer, - options: options, - handler: body - ) - } - } - """ - - 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_ServiceAClientProtocol: Sendable { - /// Documentation for MethodA - func methodA( - 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: 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: 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: GRPCProtobuf.ProtobufSerializer(), - deserializer: GRPCProtobuf.ProtobufDeserializer(), - options: options, - body - ) - } - - package func methodB( - 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: 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 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 { - private let client: GRPCCore.GRPCClient - - package init(wrapping client: GRPCCore.GRPCClient) { - self.client = client - } - - /// Documentation for MethodA - package func methodA( - 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: NamespaceA_ServiceA.Method.MethodA.descriptor, - serializer: serializer, - deserializer: deserializer, - options: options, - handler: body - ) - } - - /// Documentation for MethodB - package func methodB( - 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: NamespaceA_ServiceA.Method.MethodB.descriptor, - serializer: serializer, - deserializer: deserializer, - options: options, - handler: body - ) - } - } - """ - - 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 ServiceAClientProtocol: Sendable { - /// Documentation for MethodA - func methodA( - 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: 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: 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 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 + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message } ) async throws -> Result where Result: Sendable { - let request = GRPCCore.ClientRequest.Single( + let request = GRPCCore.ClientRequest( 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 { - private let client: GRPCCore.GRPCClient - - internal init(wrapping client: GRPCCore.GRPCClient) { - self.client = client - } - - /// Documentation for MethodA - internal func methodA( - 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: ServiceA.Method.MethodA.descriptor, - serializer: serializer, - deserializer: deserializer, - options: options, - handler: body + 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_ServiceAClientProtocol: 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_ServiceAClient: 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 ServiceBClientProtocol: 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 ServiceBClient: 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 - ) + let rendered = self.render(accessLevel: .public, service: service) + #expect(rendered == expectedSwift) } - private func assertClientCodeTranslation( - codeGenerationRequest: 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) + @available(gRPCSwift 2.0, *) + private func render( + accessLevel: AccessModifier, + service: ServiceDescriptor + ) -> String { + let translator = ClientCodeTranslator() + let codeBlocks = translator.translate( + accessModifier: accessLevel, + service: service, + availability: .macOS15Aligned + ) { + "GRPCProtobuf.ProtobufSerializer<\($0)>()" + } deserializer: { + "GRPCProtobuf.ProtobufDeserializer<\($0)>()" + } let renderer = TextBasedRenderer.default renderer.renderCodeBlocks(codeBlocks) - let contents = renderer.renderedContents() - try XCTAssertEqualWithDiff(contents, expectedSwift, file: file, line: line) + return renderer.renderedContents() } } - -#endif // os(macOS) || os(Linux) diff --git a/Tests/GRPCCodeGenTests/Internal/Translator/DocsTests.swift b/Tests/GRPCCodeGenTests/Internal/Translator/DocsTests.swift new file mode 100644 index 000000000..c100582a6 --- /dev/null +++ b/Tests/GRPCCodeGenTests/Internal/Translator/DocsTests.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 GRPCCodeGen +import Testing + +@Suite("Docs tests") +struct DocsTests { + @Test("Suffix with additional docs") + @available(gRPCSwift 2.0, *) + 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") + @available(gRPCSwift 2.0, *) + func suffixWithEmptyAdditional() { + let foo = """ + /// Foo + """ + + let additional = "" + #expect(Docs.suffix(foo, withDocs: additional) == foo) + } + + @Test("Interpose additional docs") + @available(gRPCSwift 2.0, *) + 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") + @available(gRPCSwift 2.0, *) + 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 c11dd3bda..33f4b0d87 100644 --- a/Tests/GRPCCodeGenTests/Internal/Translator/IDLToStructuredSwiftTranslatorSnippetBasedTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/Translator/IDLToStructuredSwiftTranslatorSnippetBasedTests.swift @@ -20,176 +20,15 @@ import XCTest @testable import GRPCCodeGen +@available(gRPCSwift 2.0, *) 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)) - dependencies.append( - CodeGenerationRequest.Dependency( - item: .init(kind: .typealias, name: "Bar"), - module: "Foo", - accessLevel: .internal - ) - ) - dependencies.append( - CodeGenerationRequest.Dependency( - item: .init(kind: .struct, name: "Baz"), - module: "Foo", - accessLevel: .package - ) - ) - dependencies.append( - CodeGenerationRequest.Dependency( - item: .init(kind: .class, name: "Bac"), - module: "Foo", - accessLevel: .package - ) - ) - dependencies.append( - CodeGenerationRequest.Dependency( - item: .init(kind: .enum, name: "Bap"), - module: "Foo", - accessLevel: .package - ) - ) - dependencies.append( - CodeGenerationRequest.Dependency( - item: .init(kind: .protocol, name: "Bat"), - module: "Foo", - accessLevel: .package - ) - ) - dependencies.append( - CodeGenerationRequest.Dependency( - item: .init(kind: .let, name: "Baq"), - module: "Foo", - accessLevel: .package - ) - ) - dependencies.append( - CodeGenerationRequest.Dependency( - item: .init(kind: .var, name: "Bag"), - module: "Foo", - accessLevel: .package - ) - ) - dependencies.append( - CodeGenerationRequest.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 = [CodeGenerationRequest.Dependency]() - dependencies.append( - CodeGenerationRequest.Dependency( - module: "Foo", - preconcurrency: .required, - accessLevel: .internal - ) - ) - dependencies.append( - CodeGenerationRequest.Dependency( - item: .init(kind: .enum, name: "Bar"), - module: "Foo", - preconcurrency: .required, - accessLevel: .internal - ) - ) - dependencies.append( - CodeGenerationRequest.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 = [CodeGenerationRequest.Dependency]() - dependencies.append( - CodeGenerationRequest.Dependency(module: "Foo", spi: "Secret", accessLevel: .internal) - ) - dependencies.append( - CodeGenerationRequest.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 = [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", @@ -199,11 +38,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: [] ) @@ -216,42 +54,85 @@ final class IDLToStructuredSwiftTranslatorSnippetBasedTests: XCTestCase { @_spi(Secret) internal import Foo @_spi(Secret) internal import enum Foo.Bar + // 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 { - public static let descriptor = GRPCCore.ServiceDescriptor.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] = [] } - @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 } + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension GRPCCore.ServiceDescriptor { - public static let namespaceA_ServiceA = Self( - package: "namespaceA", - service: "ServiceA" - ) + /// Service descriptor for the "namespaceA.ServiceA" service. + public static let namespaceA_ServiceA = GRPCCore.ServiceDescriptor(fullyQualifiedService: "namespaceA.ServiceA") } - /// Documentation for AService + // MARK: namespaceA.ServiceA (server) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - public protocol NamespaceA_ServiceAStreamingServiceProtocol: GRPCCore.RegistrableRPCService {} + extension NamespaceA_ServiceA { + /// 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 {} + + /// 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 {} + + /// 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 {} + } - /// Conformance to `GRPCCore.RegistrableRPCService`. + // 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 { - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - public func registerMethods(with router: inout GRPCCore.RPCRouter) {} + public func registerMethods(with router: inout GRPCCore.RPCRouter) where Transport: GRPCCore.ServerTransport {} } - /// Documentation for AService + // Default implementation of streaming methods from 'StreamingServiceProtocol'. @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - public protocol NamespaceA_ServiceAServiceProtocol: NamespaceA_ServiceA.StreamingServiceProtocol {} + extension NamespaceA_ServiceA.ServiceProtocol { + } - /// Partial conformance to `NamespaceA_ServiceAStreamingServiceProtocol`. + // 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.ServiceProtocol { + extension NamespaceA_ServiceA.SimpleServiceProtocol { } """ try self.assertIDLToStructuredSwiftTranslation( @@ -265,11 +146,108 @@ 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()), + availability: .macOS15Aligned + ) + let renderer = TextBasedRenderer.default + let sourceFile = try renderer.render(structured: structuredSwift) + let contents = sourceFile.contents + + XCTAssertFalse(contents.contains("GRPCCore")) + } + + 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, - accessLevel: SourceGenerator.Config.AccessLevel, - server: Bool = false + accessLevel: CodeGenerator.Config.AccessLevel, + server: Bool = false, + grpcCoreModuleName: String = "GRPCCore" ) throws { let translator = IDLToStructuredSwiftTranslator() let structuredSwift = try translator.translate( @@ -277,7 +255,9 @@ final class IDLToStructuredSwiftTranslatorSnippetBasedTests: XCTestCase { accessLevel: accessLevel, accessLevelOnImports: true, client: false, - server: server + server: server, + grpcCoreModuleName: grpcCoreModuleName, + availability: .macOS15Aligned ) let renderer = TextBasedRenderer.default let sourceFile = try renderer.render(structured: structuredSwift) @@ -288,8 +268,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: [] ) @@ -302,7 +285,9 @@ final class IDLToStructuredSwiftTranslatorSnippetBasedTests: XCTestCase { accessLevel: .public, accessLevelOnImports: true, client: true, - server: true + server: true, + grpcCoreModuleName: "GRPCCore", + availability: .macOS15Aligned ) ) { error in @@ -322,15 +307,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: [] ) @@ -343,7 +334,9 @@ final class IDLToStructuredSwiftTranslatorSnippetBasedTests: XCTestCase { accessLevel: .public, accessLevelOnImports: true, client: true, - server: true + server: true, + grpcCoreModuleName: "GRPCCore", + availability: .macOS15Aligned ) ) { error in @@ -361,11 +354,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: [] ) @@ -379,7 +371,9 @@ final class IDLToStructuredSwiftTranslatorSnippetBasedTests: XCTestCase { accessLevel: .public, accessLevelOnImports: true, client: true, - server: true + server: true, + grpcCoreModuleName: "GRPCCore", + availability: .macOS15Aligned ) ) { error in @@ -399,21 +393,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: [] ) @@ -427,10 +419,11 @@ final class IDLToStructuredSwiftTranslatorSnippetBasedTests: XCTestCase { accessLevel: .internal, accessLevelOnImports: true, client: true, - server: true + server: true, + grpcCoreModuleName: "GRPCCore", + availability: .macOS15Aligned ) - ) { - error in + ) { error in XCTAssertEqual( error as CodeGenError, CodeGenError( @@ -447,7 +440,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", @@ -455,11 +448,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] ) @@ -473,17 +465,18 @@ final class IDLToStructuredSwiftTranslatorSnippetBasedTests: XCTestCase { accessLevel: .public, accessLevelOnImports: true, client: true, - server: true + server: true, + grpcCoreModuleName: "GRPCCore", + availability: .macOS15Aligned ) - ) { - 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. """ ) ) @@ -493,7 +486,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", @@ -501,7 +498,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", @@ -509,11 +510,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] ) @@ -527,17 +527,19 @@ final class IDLToStructuredSwiftTranslatorSnippetBasedTests: XCTestCase { accessLevel: .public, accessLevelOnImports: true, client: true, - server: true + server: true, + grpcCoreModuleName: "GRPCCore", + availability: .macOS15Aligned ) - ) { - 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. """ ) ) @@ -547,7 +549,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", @@ -555,7 +557,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", @@ -563,11 +565,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] ) @@ -581,7 +582,9 @@ final class IDLToStructuredSwiftTranslatorSnippetBasedTests: XCTestCase { accessLevel: .public, accessLevelOnImports: true, client: true, - server: true + server: true, + grpcCoreModuleName: "GRPCCore", + availability: .macOS15Aligned ) ) { error in @@ -591,7 +594,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. """ ) ) @@ -601,21 +605,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: [] ) @@ -628,7 +630,9 @@ final class IDLToStructuredSwiftTranslatorSnippetBasedTests: XCTestCase { accessLevel: .public, accessLevelOnImports: true, client: true, - server: true + server: true, + grpcCoreModuleName: "GRPCCore", + availability: .macOS15Aligned ) ) { error in diff --git a/Tests/GRPCCodeGenTests/Internal/Translator/ServerCodeTranslatorSnippetBasedTests.swift b/Tests/GRPCCodeGenTests/Internal/Translator/ServerCodeTranslatorSnippetBasedTests.swift index d4d2bc571..9db99776f 100644 --- a/Tests/GRPCCodeGenTests/Internal/Translator/ServerCodeTranslatorSnippetBasedTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/Translator/ServerCodeTranslatorSnippetBasedTests.swift @@ -14,429 +14,141 @@ * 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 { - typealias MethodDescriptor = GRPCCodeGen.CodeGenerationRequest.ServiceDescriptor.MethodDescriptor - typealias ServiceDescriptor = GRPCCodeGen.CodeGenerationRequest.ServiceDescriptor - typealias Name = GRPCCodeGen.CodeGenerationRequest.Name - - func testServerCodeTranslatorUnaryMethod() throws { +@Suite +final class ServerCodeTranslatorSnippetBasedTests { + @Test + @available(gRPCSwift 2.0, *) + 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", outputType: "NamespaceA_ServiceAResponse" ) - let service = ServiceDescriptor( - documentation: "/// Documentation for ServiceA", - name: Name( - base: "AlongNameForServiceA", - 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_ServiceAStreamingServiceProtocol: GRPCCore.RegistrableRPCService { - /// Documentation for unaryMethod - 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, *) - 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, - deserializer: GRPCProtobuf.ProtobufDeserializer(), - serializer: GRPCProtobuf.ProtobufSerializer(), - handler: { request, context in - try await self.unary( - 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_ServiceAServiceProtocol: 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`. - @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, - 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) - } - } - """ - - 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" + name: ServiceName( + identifyingName: "namespaceA.AlongNameForServiceA", + typeName: "NamespaceA_ServiceA", + propertyName: "namespaceA_serviceA" ), 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_ServiceAStreamingServiceProtocol: GRPCCore.RegistrableRPCService { - /// Documentation for inputStreamingMethod - 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, *) - 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_ServiceAServiceProtocol: 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`. - @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, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse.Stream { - let response = try await self.inputStreaming( - request: request, - context: context - ) - return GRPCCore.ServerResponse.Stream(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_ServiceAStreamingServiceProtocol: GRPCCore.RegistrableRPCService { - /// Documentation for outputStreamingMethod - 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, *) - 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_ServiceAServiceProtocol: 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`. - @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, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse.Stream { - let response = try await self.outputStreaming( - request: GRPCCore.ServerRequest.Single(stream: request), - context: context - ) - return response + 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. + /// + /// 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 { + /// 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 } - } - """ - - 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_ServiceAStreamingServiceProtocol: GRPCCore.RegistrableRPCService { - /// Documentation for bidirectionalStreamingMethod - 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, *) - 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 - ) - } - ) + /// 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 { + /// 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 } - } - /// 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 { - /// Documentation for bidirectionalStreamingMethod - 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, *) - 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_ServiceAStreamingServiceProtocol: GRPCCore.RegistrableRPCService { - /// Documentation for inputStreamingMethod - func inputStreaming( - request: GRPCCore.ServerRequest.Stream, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse.Stream - - /// Documentation for outputStreamingMethod - func outputStreaming( - request: GRPCCore.ServerRequest.Stream, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse.Stream + /// 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 + } } - /// Conformance to `GRPCCore.RegistrableRPCService`. + // 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 { - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - internal 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.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, + forMethod: NamespaceA_ServiceA.Method.Unary.descriptor, deserializer: GRPCProtobuf.ProtobufDeserializer(), serializer: GRPCProtobuf.ProtobufSerializer(), handler: { request, context in - try await self.outputStreaming( + try await self.unary( request: request, context: context ) @@ -444,211 +156,59 @@ 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 { - /// Documentation for inputStreamingMethod - func inputStreaming( - request: GRPCCore.ServerRequest.Stream, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse.Single - - /// Documentation for outputStreamingMethod - func outputStreaming( - request: GRPCCore.ServerRequest.Single, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse.Stream - } - /// Partial conformance to `NamespaceA_ServiceAStreamingServiceProtocol`. + // 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 { - 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, + public func unary( + request: GRPCCore.StreamingServerRequest, context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse.Stream { - let response = try await self.outputStreaming( - request: GRPCCore.ServerRequest.Single(stream: request), + ) async throws -> GRPCCore.StreamingServerResponse { + let response = try await self.unary( + 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 ServiceAStreamingServiceProtocol: GRPCCore.RegistrableRPCService { - /// Documentation for MethodA - 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, *) - 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 - ) - } - ) + return GRPCCore.StreamingServerResponse(single: response) } } - /// Documentation for ServiceA + // Default implementation of methods from 'ServiceProtocol'. @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, - 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, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse.Stream { - let response = try await self.methodA( - request: GRPCCore.ServerRequest.Single(stream: request), - context: context + 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: [:] ) - return GRPCCore.ServerResponse.Stream(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_ServiceAStreamingServiceProtocol: 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_ServiceAServiceProtocol: NamespaceA_ServiceA.StreamingServiceProtocol {} - /// 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 { - } - /// 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 {} - /// 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_ServiceBServiceProtocol: NamespaceA_ServiceB.StreamingServiceProtocol {} - /// Partial conformance to `NamespaceA_ServiceBStreamingServiceProtocol`. - @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: CodeGenerationRequest, - expectedSwift: String, - accessLevel: SourceGenerator.Config.AccessLevel - ) throws { - let translator = ServerCodeTranslator(accessLevel: accessLevel) - let codeBlocks = try translator.translate(from: codeGenerationRequest) + @available(gRPCSwift 2.0, *) + private func render( + accessLevel: AccessModifier, + service: ServiceDescriptor + ) -> String { + let translator = ServerCodeTranslator() + let codeBlocks = translator.translate( + accessModifier: accessLevel, + service: service, + availability: .macOS15Aligned + ) { + "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) diff --git a/Tests/GRPCCodeGenTests/Internal/Translator/TestFunctions.swift b/Tests/GRPCCodeGenTests/Internal/Translator/TestFunctions.swift index 52ab821a1..ee516e0e3 100644 --- a/Tests/GRPCCodeGenTests/Internal/Translator/TestFunctions.swift +++ b/Tests/GRPCCodeGenTests/Internal/Translator/TestFunctions.swift @@ -71,19 +71,20 @@ internal func XCTAssertEqualWithDiff( ) } +@available(gRPCSwift 2.0, *) internal func makeCodeGenerationRequest( - services: [CodeGenerationRequest.ServiceDescriptor] = [], - dependencies: [CodeGenerationRequest.Dependency] = [] + services: [ServiceDescriptor] = [], + dependencies: [Dependency] = [] ) -> CodeGenerationRequest { return CodeGenerationRequest( fileName: "test.grpc", 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 af0a9017a..59250a2e4 100644 --- a/Tests/GRPCCodeGenTests/Internal/Translator/TypealiasTranslatorSnippetBasedTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/Translator/TypealiasTranslatorSnippetBasedTests.swift @@ -14,21 +14,18 @@ * 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 { - typealias MethodDescriptor = GRPCCodeGen.CodeGenerationRequest.ServiceDescriptor.MethodDescriptor - typealias ServiceDescriptor = GRPCCodeGen.CodeGenerationRequest.ServiceDescriptor - typealias Name = GRPCCodeGen.CodeGenerationRequest.Name - +@Suite +struct TypealiasTranslatorSnippetBasedTests { + @Test + @available(gRPCSwift 2.0, *) 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", @@ -36,704 +33,66 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { ) 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] ) - 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 static let descriptors: [GRPCCore.MethodDescriptor] = [ - MethodA.descriptor - ] - } - @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 - @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 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] = [] - } - @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 - @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 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] = [] - } - @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 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] = [] - } - @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 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, - method: "MethodA" - ) - } - public static let descriptors: [GRPCCore.MethodDescriptor] = [ - MethodA.descriptor - ] - } - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - public typealias StreamingServiceProtocol = ServiceAStreamingServiceProtocol - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - public typealias ServiceProtocol = ServiceAServiceProtocol - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - public typealias ClientProtocol = ServiceAClientProtocol - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - public typealias Client = ServiceAClient - } - extension GRPCCore.ServiceDescriptor { - public static let ServiceA = Self( - package: "", - service: "ServiceA" - ) - } - """ - - try self.assertTypealiasTranslation( - codeGenerationRequest: makeCodeGenerationRequest(services: [service]), - expectedSwift: expectedSwift, - client: true, - server: true, - accessLevel: .public - ) - } - - 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 = - """ + 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 { - public static let descriptor = GRPCCore.ServiceDescriptor.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: NamespaceA_ServiceA.descriptor.fullyQualifiedService, + service: GRPCCore.ServiceDescriptor(fullyQualifiedService: "namespaceA.ServiceA"), 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" - ) - } + /// Descriptors for all methods in the "namespaceA.ServiceA" service. public static let descriptors: [GRPCCore.MethodDescriptor] = [ - MethodA.descriptor, - MethodB.descriptor + MethodA.descriptor ] } - @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 - @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 } + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension GRPCCore.ServiceDescriptor { - public static let namespaceA_ServiceA = Self( - package: "namespaceA", - service: "ServiceA" - ) + /// Service descriptor for the "namespaceA.ServiceA" service. + 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] = [] - } - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - package typealias StreamingServiceProtocol = NamespaceA_ServiceAStreamingServiceProtocol - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - package typealias ServiceProtocol = NamespaceA_ServiceAServiceProtocol - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - package typealias ClientProtocol = NamespaceA_ServiceAClientProtocol - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - package typealias Client = NamespaceA_ServiceAClient - } - 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 - ) - } - - 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_AserviceStreamingServiceProtocol - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - public typealias ServiceProtocol = NamespaceA_AserviceServiceProtocol - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - public typealias ClientProtocol = NamespaceA_AserviceClientProtocol - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - public typealias Client = NamespaceA_AserviceClient - } - 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_BserviceStreamingServiceProtocol - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - public typealias ServiceProtocol = NamespaceA_BserviceServiceProtocol - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - public typealias ClientProtocol = NamespaceA_BserviceClientProtocol - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - public typealias Client = NamespaceA_BserviceClient - } - 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 = AServiceStreamingServiceProtocol - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - package typealias ServiceProtocol = AServiceServiceProtocol - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - package typealias ClientProtocol = AServiceClientProtocol - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - package typealias Client = AServiceClient - } - 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 = BServiceStreamingServiceProtocol - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - package typealias ServiceProtocol = BServiceServiceProtocol - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - package typealias ClientProtocol = BServiceClientProtocol - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - package typealias Client = BServiceClient - } - 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_AServiceStreamingServiceProtocol - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - internal typealias ServiceProtocol = Anamespace_AServiceServiceProtocol - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - internal typealias ClientProtocol = Anamespace_AServiceClientProtocol - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - internal typealias Client = Anamespace_AServiceClient - } - 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_BServiceStreamingServiceProtocol - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - internal typealias ServiceProtocol = Bnamespace_BServiceServiceProtocol - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - internal typealias ClientProtocol = Bnamespace_BServiceClientProtocol - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - internal typealias Client = Bnamespace_BServiceClient - } - 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_AServiceStreamingServiceProtocol - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - public typealias ServiceProtocol = Anamespace_AServiceServiceProtocol - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - public typealias ClientProtocol = Anamespace_AServiceClientProtocol - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - public typealias Client = Anamespace_AServiceClient - } - 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 = BServiceStreamingServiceProtocol - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - public typealias ServiceProtocol = BServiceServiceProtocol - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - public typealias ClientProtocol = BServiceClientProtocol - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - public typealias Client = BServiceClient - } - 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 - ) + #expect(self.render(accessLevel: .public, service: service) == expectedSwift) } } +@available(gRPCSwift 2.0, *) extension TypealiasTranslatorSnippetBasedTests { - private func assertTypealiasTranslation( - codeGenerationRequest: 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) + func render( + accessLevel: CodeGenerator.Config.AccessLevel, + service: ServiceDescriptor + ) -> String { + let translator = MetadataTranslator() + let codeBlocks = translator.translate( + accessModifier: AccessModifier(accessLevel), + service: service, + availability: .macOS15Aligned + ) + let renderer = TextBasedRenderer.default renderer.renderCodeBlocks(codeBlocks) - let contents = renderer.renderedContents() - try XCTAssertEqualWithDiff(contents, expectedSwift) + return renderer.renderedContents() } } - -#endif // os(macOS) || os(Linux) diff --git a/Tests/GRPCCoreTests/Call/Client/ClientRequestTests.swift b/Tests/GRPCCoreTests/Call/Client/ClientRequestTests.swift index 7d0304260..677228131 100644 --- a/Tests/GRPCCoreTests/Call/Client/ClientRequestTests.swift +++ b/Tests/GRPCCoreTests/Call/Client/ClientRequestTests.swift @@ -18,12 +18,12 @@ import XCTest @testable import GRPCCore -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +@available(gRPCSwift 2.0, *) 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..30df1465c 100644 --- a/Tests/GRPCCoreTests/Call/Client/ClientResponseTests.swift +++ b/Tests/GRPCCoreTests/Call/Client/ClientResponseTests.swift @@ -18,10 +18,10 @@ import XCTest @testable import GRPCCore -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +@available(gRPCSwift 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) { @@ -54,8 +54,8 @@ final class ClientResponseTests: XCTestCase { XCTAssertEqual(response.trailingMetadata, ["bar": "baz"]) } - func testAcceptedStreamResponseConvenienceMethods() async throws { - let response = ClientResponse.Stream( + func testAcceptedStreamResponseConvenienceMethods_Messages() async throws { + let response = StreamingClientResponse( of: String.self, metadata: ["foo": "bar"], bodyParts: RPCAsyncSequence( @@ -74,9 +74,32 @@ 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 = ClientResponse.Stream(of: String.self, error: error) + let response = StreamingClientResponse(of: String.self, error: error) XCTAssertEqual(response.metadata, [:]) await XCTAssertThrowsRPCErrorAsync { @@ -84,16 +107,21 @@ final class ClientResponseTests: XCTestCase { } errorHandler: { XCTAssertEqual($0, error) } + await XCTAssertThrowsRPCErrorAsync { + try await response.bodyParts.collect() + } errorHandler: { + XCTAssertEqual($0, error) + } } 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 +129,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 +140,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 +162,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 +186,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+ServerBehavior.swift b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness+ServerBehavior.swift index 0c2ab936f..11d661dde 100644 --- a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness+ServerBehavior.swift +++ b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness+ServerBehavior.swift @@ -18,22 +18,22 @@ import XCTest @testable import GRPCCore -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +@available(gRPCSwift 2.0, *) extension ClientRPCExecutorTestHarness { struct ServerStreamHandler: Sendable { 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 ) { @@ -44,9 +44,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, @@ -59,11 +59,11 @@ extension ClientRPCExecutorTestHarness { } } -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +@available(gRPCSwift 2.0, *) 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) @@ -114,7 +114,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/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness+Transport.swift b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness+Transport.swift index 0500b23ec..c1ae7869a 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. @@ -17,11 +17,11 @@ import GRPCCore import GRPCInProcessTransport -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -extension InProcessServerTransport { +@available(gRPCSwift 2.0, *) +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, peer: self.peer) } } diff --git a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness.swift b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness.swift index 06f200ae6..18a5d06c2 100644 --- a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness.swift +++ b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness.swift @@ -25,11 +25,12 @@ 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, *) +@available(gRPCSwift 2.0, *) 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 @@ -43,18 +44,23 @@ 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: - let server = InProcessServerTransport() + 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 = InProcessServerTransport() // 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) @@ -67,41 +73,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 +123,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 { @@ -133,12 +143,12 @@ struct ClientRPCExecutorTestHarness { // Execute the request. try await ClientRPCExecutor.execute( request: request, - method: MethodDescriptor(service: "foo", method: "bar"), + method: MethodDescriptor(fullyQualifiedService: "foo", method: "bar"), options: options, serializer: serializer, deserializer: deserializer, transport: self.clientTransport, - interceptors: [], + interceptors: self.interceptors, handler: handler ) diff --git a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTests+Hedging.swift b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTests+Hedging.swift index 6dceb9976..77a64e9e9 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 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +@available(gRPCSwift 2.0, *) extension ClientRPCExecutorTests { func testHedgingWhenAllAttemptsResultInNonFatalCodes() async throws { let harness = ClientRPCExecutorTestHarness( @@ -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]) @@ -186,7 +186,7 @@ extension ClientRPCExecutorTests { } } -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@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 6c2e6cd1e..e8abaa857 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 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +@available(gRPCSwift 2.0, *) extension ClientRPCExecutorTests { fileprivate func makeHarnessForRetries( rejectUntilAttempt firstSuccessfulAttempt: Int, @@ -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) @@ -289,7 +289,7 @@ extension ClientRPCExecutorTests { } } -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@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 c2396fdef..474640f95 100644 --- a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTests.swift +++ b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTests.swift @@ -18,12 +18,12 @@ import XCTest @testable import GRPCCore -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +@available(gRPCSwift 2.0, *) 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"]) @@ -269,4 +269,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/Client/RetryDelaySequenceTests.swift b/Tests/GRPCCoreTests/Call/Client/RetryDelaySequenceTests.swift index cbe0d7a09..4811700df 100644 --- a/Tests/GRPCCoreTests/Call/Client/RetryDelaySequenceTests.swift +++ b/Tests/GRPCCoreTests/Call/Client/RetryDelaySequenceTests.swift @@ -17,7 +17,7 @@ import XCTest @testable import GRPCCore -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@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 new file mode 100644 index 000000000..bbaa05d05 --- /dev/null +++ b/Tests/GRPCCoreTests/Call/ConditionalInterceptorTests.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 +import Testing + +@Suite("ConditionalInterceptor") +struct ConditionalInterceptorTests { + @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 [(ConditionalInterceptor.Subject, [MethodDescriptor], [MethodDescriptor])] + ) + @available(gRPCSwift 2.0, *) + func appliesTo( + target: ConditionalInterceptor.Subject, + applicableMethods: [MethodDescriptor], + notApplicableMethods: [MethodDescriptor] + ) { + for applicableMethod in applicableMethods { + #expect(target.applies(to: applicableMethod)) + } + + for notApplicableMethod in notApplicableMethods { + #expect(!target.applies(to: notApplicableMethod)) + } + } +} + +@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") + 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/Call/Server/Internal/ServerCancellationManagerTests.swift b/Tests/GRPCCoreTests/Call/Server/Internal/ServerCancellationManagerTests.swift new file mode 100644 index 000000000..87f8b200a --- /dev/null +++ b/Tests/GRPCCoreTests/Call/Server/Internal/ServerCancellationManagerTests.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 GRPCCore +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() + #expect(manager.isRPCCancelled) + } + + @Test("Cancellation handler runs") + @available(gRPCSwift 2.0, *) + 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") + @available(gRPCSwift 2.0, *) + 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") + @available(gRPCSwift 2.0, *) + func removeCancellationHandler() async throws { + let manager = ServerCancellationManager() + + let id = manager.addRPCCancelledHandler { + Issue.record("Unexpected cancellation") + } + + #expect(id != nil) + manager.removeRPCCancelledHandler(withID: id!) + manager.cancelRPC() + } + + @Test("Wait for cancellation") + @available(gRPCSwift 2.0, *) + 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 8d7e0a543..f0f156c2a 100644 --- a/Tests/GRPCCoreTests/Call/Server/Internal/ServerRPCExecutorTestSupport/ServerRPCExecutorTestHarness.swift +++ b/Tests/GRPCCoreTests/Call/Server/Internal/ServerRPCExecutorTestSupport/ServerRPCExecutorTestHarness.swift @@ -17,27 +17,33 @@ import XCTest @testable import GRPCCore -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +@available(gRPCSwift 2.0, *) struct ServerRPCExecutorTestHarness { struct ServerHandler: Sendable { - let fn: @Sendable (ServerRequest.Stream) async throws -> ServerResponse.Stream + let fn: + @Sendable ( + _ request: StreamingServerRequest, + _ context: ServerContext + ) async throws -> StreamingServerResponse init( _ fn: @escaping @Sendable ( - ServerRequest.Stream - ) async throws -> ServerResponse.Stream + _ request: StreamingServerRequest, + _ context: ServerContext + ) async throws -> StreamingServerResponse ) { self.fn = fn } func handle( - _ request: ServerRequest.Stream - ) async throws -> ServerResponse.Stream { - try await self.fn(request) + _ request: StreamingServerRequest, + _ context: ServerContext + ) async throws -> StreamingServerResponse { + try await self.fn(request, context) } static func throwing(_ error: any Error) -> Self { - return Self { _ in throw error } + return Self { _, _ in throw error } } } @@ -47,17 +53,19 @@ struct ServerRPCExecutorTestHarness { self.interceptors = interceptors } - func execute( + func execute( + bytes: Bytes.Type = Bytes.self, deserializer: some MessageDeserializer, serializer: some MessageSerializer, handler: @escaping @Sendable ( - ServerRequest.Stream - ) async throws -> ServerResponse.Stream, + StreamingServerRequest, + 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( @@ -69,19 +77,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 { @@ -93,21 +101,29 @@ 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(fullyQualifiedService: "foo", method: "bar"), + remotePeer: "remote", + localPeer: "local", + 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() @@ -117,10 +133,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( @@ -133,11 +149,11 @@ struct ServerRPCExecutorTestHarness { } } -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +@available(gRPCSwift 2.0, *) extension ServerRPCExecutorTestHarness.ServerHandler where Input == Output { static var echo: Self { - return Self { request in - return ServerResponse.Stream(metadata: request.metadata) { writer 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 5d2aa0029..73f9ba82f 100644 --- a/Tests/GRPCCoreTests/Call/Server/Internal/ServerRPCExecutorTests.swift +++ b/Tests/GRPCCoreTests/Call/Server/Internal/ServerRPCExecutorTests.swift @@ -18,7 +18,7 @@ import XCTest @testable import GRPCCore -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +@available(gRPCSwift 2.0, *) final class ServerRPCExecutorTests: XCTestCase { func testEchoNoMessages() async throws { let harness = ServerRPCExecutorTestHarness() @@ -82,12 +82,13 @@ final class ServerRPCExecutorTests: XCTestCase { func testEchoSingleJSONMessage() async throws { let harness = ServerRPCExecutorTestHarness() try await harness.execute( + bytes: [UInt8].self, deserializer: JSONDeserializer(), serializer: JSONSerializer() - ) { request in + ) { 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 [:] } @@ -111,12 +112,13 @@ final class ServerRPCExecutorTests: XCTestCase { func testEchoMultipleJSONMessages() async throws { let harness = ServerRPCExecutorTestHarness() try await harness.execute( + bytes: [UInt8].self, deserializer: JSONDeserializer(), serializer: JSONSerializer() - ) { request in + ) { 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 [:] @@ -143,10 +145,11 @@ final class ServerRPCExecutorTests: XCTestCase { func testReturnTrailingMetadata() async throws { let harness = ServerRPCExecutorTestHarness() try await harness.execute( + bytes: [UInt8].self, deserializer: IdentityDeserializer(), serializer: IdentitySerializer() - ) { request in - return ServerResponse.Stream(metadata: request.metadata) { _ in + ) { request, _ in + return StreamingServerResponse(metadata: request.metadata) { _ in return ["bar": "baz"] } } producer: { inbound in @@ -234,17 +237,12 @@ final class ServerRPCExecutorTests: XCTestCase { func testHandlerRespectsTimeout() async throws { let harness = ServerRPCExecutorTestHarness() try await harness.execute( + bytes: [UInt8].self, 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: "")) + ) { 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() @@ -252,7 +250,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") } } } @@ -267,11 +265,12 @@ 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 + ) { request, _ in XCTFail("Unexpected request") - return ServerResponse.Stream( + return StreamingServerResponse( of: [UInt8].self, error: RPCError(code: .failedPrecondition, message: "") ) @@ -340,7 +339,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 @@ -351,4 +352,46 @@ 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"]) + ] + ) + } + } + + 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/Call/Server/RPCRouterTests.swift b/Tests/GRPCCoreTests/Call/Server/RPCRouterTests.swift index 86fe1bd1e..5ec97ddbf 100644 --- a/Tests/GRPCCoreTests/Call/Server/RPCRouterTests.swift +++ b/Tests/GRPCCoreTests/Call/Server/RPCRouterTests.swift @@ -17,19 +17,23 @@ import GRPCCore import XCTest -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +@available(gRPCSwift 2.0, *) final class RPCRouterTests: XCTestCase { func testEmptyRouter() async throws { - var router = RPCRouter() + 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"))) + XCTAssertFalse( + router.hasHandler(forMethod: MethodDescriptor(fullyQualifiedService: "foo", method: "bar")) + ) + XCTAssertFalse( + router.removeHandler(forMethod: MethodDescriptor(fullyQualifiedService: "foo", method: "bar")) + ) } func testRegisterMethod() async throws { - var router = RPCRouter() - let method = MethodDescriptor(service: "foo", method: "bar") + var router = RPCRouter() + let method = MethodDescriptor(fullyQualifiedService: "foo", method: "bar") router.registerHandler( forMethod: method, deserializer: IdentityDeserializer(), @@ -44,8 +48,8 @@ final class RPCRouterTests: XCTestCase { } func testRemoveMethod() async throws { - var router = RPCRouter() - let method = MethodDescriptor(service: "foo", method: "bar") + var router = RPCRouter() + let method = MethodDescriptor(fullyQualifiedService: "foo", method: "bar") router.registerHandler( forMethod: method, deserializer: IdentityDeserializer(), @@ -60,3 +64,19 @@ final class RPCRouterTests: XCTestCase { XCTAssertEqual(router.methods, []) } } + +@available(gRPCSwift 2.0, *) +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/Call/Server/ServerContextTests.swift b/Tests/GRPCCoreTests/Call/Server/ServerContextTests.swift new file mode 100644 index 000000000..f575d8f30 --- /dev/null +++ b/Tests/GRPCCoreTests/Call/Server/ServerContextTests.swift @@ -0,0 +1,65 @@ +/* + * Copyright 2024, gRPC Authors All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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") + @available(gRPCSwift 2.0, *) + func isCancelled() async throws { + await withServerContextRPCCancellationHandle { handle in + #expect(!handle.isCancelled) + handle.cancel() + #expect(handle.isCancelled) + } + } + + @Test("Wait for cancellation") + @available(gRPCSwift 2.0, *) + 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") + @available(gRPCSwift 2.0, *) + 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/GRPCCoreTests/Call/Server/ServerRequestTests.swift b/Tests/GRPCCoreTests/Call/Server/ServerRequestTests.swift index 532e5e51c..c83aa3fc7 100644 --- a/Tests/GRPCCoreTests/Call/Server/ServerRequestTests.swift +++ b/Tests/GRPCCoreTests/Call/Server/ServerRequestTests.swift @@ -16,11 +16,11 @@ @_spi(Testing) import GRPCCore import XCTest -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +@available(gRPCSwift 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..0267405c3 100644 --- a/Tests/GRPCCoreTests/Call/Server/ServerResponseTests.swift +++ b/Tests/GRPCCoreTests/Call/Server/ServerResponseTests.swift @@ -13,70 +13,80 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -@_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( +import GRPCCore +import Testing + +@Suite("ServerResponse") +struct ServerResponseTests { + @Test("ServerResponse(message:metadata:trailingMetadata:)") + @available(gRPCSwift 2.0, *) + 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 response.accepted.get() + #expect(contents.message == "message") + #expect(contents.metadata == ["metadata": "initial"]) + #expect(contents.trailingMetadata == ["metadata": "trailing"]) + } + @Test("ServerResponse(of:error:)") + @available(gRPCSwift 2.0, *) + func responseInitError() throws { let error = RPCError(code: .aborted, message: "Aborted") - response = ServerResponse.Single(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 = ServerResponse.Stream(of: String.self, metadata: ["metadata": "initial"]) { _ in + @Test("StreamingServerResponse(of:metadata:producer:)") + @available(gRPCSwift 2.0, *) + func streamingResponseInitSuccess() async throws { + let response = StreamingServerResponse( + 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 contents = try response.accepted.get() + #expect(contents.metadata == ["metadata": "initial"]) + let trailingMetadata = try await contents.producer(.failTestOnWrite()) + #expect(trailingMetadata == ["metadata": "trailing"]) + } + @Test("StreamingServerResponse(of:error:)") + @available(gRPCSwift 2.0, *) + func streamingResponseInitError() async throws { let error = RPCError(code: .aborted, message: "Aborted") - response = ServerResponse.Stream(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 { - let single = ServerResponse.Single( + @Test("StreamingServerResponse(single:) (accepted)") + @available(gRPCSwift 2.0, *) + func singleToStreamConversionForSuccessfulResponse() async throws { + 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 @@ -88,19 +98,52 @@ 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)") + @available(gRPCSwift 2.0, *) + func singleToStreamConversionForFailedResponse() 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) + switch stream.accepted { + case .success: + Issue.record("Expected error") + case .failure(let rpcError): + #expect(rpcError == error) + } + } + + @Test("Mutate metadata on response", arguments: [true, false]) + @available(gRPCSwift 2.0, *) + 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]) + @available(gRPCSwift 2.0, *) + 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"]) } } diff --git a/Tests/GRPCCoreTests/Coding/CodingTests.swift b/Tests/GRPCCoreTests/Coding/CodingTests.swift index efb57f94f..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. @@ -35,7 +36,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/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/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 879269999..6ee5aa88f 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 } @@ -578,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/Tests/GRPCCoreTests/Configuration/MethodConfigCodingTests.swift b/Tests/GRPCCoreTests/Configuration/MethodConfigCodingTests.swift index d9797343a..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) @@ -186,9 +192,11 @@ struct MethodConfigCodingTests { ("1s", .seconds(1)), ("1.000000s", .seconds(1)), ("0s", .zero), + ("0.1s", .milliseconds(100)), ("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) @@ -205,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 { @@ -216,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) @@ -223,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) @@ -230,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( @@ -252,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) @@ -259,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( @@ -275,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) @@ -282,6 +297,7 @@ struct MethodConfigCodingTests { } @Test("MethodConfig") + @available(gRPCSwift 2.0, *) func methodConfig() throws { let expected = MethodConfig( names: [ @@ -301,6 +317,7 @@ struct MethodConfigCodingTests { } @Test("MethodConfig with hedging") + @available(gRPCSwift 2.0, *) func methodConfigWithHedging() throws { let expected = MethodConfig( names: [ @@ -327,6 +344,7 @@ struct MethodConfigCodingTests { } @Test("MethodConfig with retries") + @available(gRPCSwift 2.0, *) func methodConfigWithRetries() throws { let expected = MethodConfig( names: [ @@ -405,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 8dbcd7340..bdf5671c0 100644 --- a/Tests/GRPCCoreTests/Configuration/ServiceConfigCodingTests.swift +++ b/Tests/GRPCCoreTests/Configuration/ServiceConfigCodingTests.swift @@ -16,9 +16,10 @@ import Foundation import GRPCCore +import SwiftProtobuf import XCTest -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@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 af566279d..9e6db0cdb 100644 --- a/Tests/GRPCCoreTests/GRPCClientTests.swift +++ b/Tests/GRPCCoreTests/GRPCClientTests.swift @@ -16,49 +16,34 @@ import GRPCCore import GRPCInProcessTransport +import Testing import XCTest -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +@available(gRPCSwift 2.0, *) final class GRPCClientTests: XCTestCase { func withInProcessConnectedClient( services: [any RegistrableRPCService], - interceptors: [any ClientInterceptor] = [], - _ body: (GRPCClient, GRPCServer) async throws -> Void + interceptorPipeline: [ConditionalInterceptor] = [], + _ body: ( + GRPCClient, + GRPCServer + ) async throws -> Void ) async throws { - let inProcess = InProcessTransport.makePair() - let client = GRPCClient(transport: inProcess.client, interceptors: interceptors) - 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() + let inProcess = InProcessTransport() + _ = GRPCClient(transport: inProcess.client, interceptorPipeline: interceptorPipeline) + _ = GRPCServer(transport: inProcess.server, services: services) + + 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), tolerance: .zero) + 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() - } - } - - 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 } } @@ -140,7 +125,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 @@ -160,7 +145,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 @@ -176,7 +161,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 @@ -196,7 +181,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 @@ -235,10 +220,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( @@ -329,7 +314,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 @@ -339,7 +324,7 @@ final class GRPCClientTests: XCTestCase { } group.addTask { - try await client.run() + try await client.runConnections() } // Wait for client and server to be running. @@ -356,8 +341,8 @@ final class GRPCClientTests: XCTestCase { let task = Task { try await client.clientStreaming( - request: ClientRequest.Stream { writer in - try await Task.sleep(for: .seconds(5)) + request: StreamingClientRequest { writer in + try await Task.sleep(for: .seconds(5), tolerance: .zero) }, descriptor: BinaryEcho.Methods.collect, serializer: IdentitySerializer(), @@ -377,32 +362,32 @@ 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() } + 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) } } 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() } + 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) { - try await client.run() + try await client.runConnections() } errorHandler: { error in XCTAssertEqual(error.code, .clientIsAlreadyRunning) } @@ -410,3 +395,164 @@ final class GRPCClientTests: XCTestCase { task.cancel() } } + +@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() + 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 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 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") + @available(gRPCSwift 2.0, *) + 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 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 response.message + #expect(message == Array("hello".utf8)) + } + + #expect(onlyBinaryEchoGetCounter.value == 1) + #expect(allMethodsCounter.value == 2) + #expect(onlyBinaryEchoCollectCounter.value == 1) + #expect(bothBinaryEchoMethodsCounter.value == 2) + } + } + + @available(gRPCSwift 2.0, *) + func withInProcessConnectedClient( + services: [any RegistrableRPCService], + interceptorPipeline: [ConditionalInterceptor] = [], + _ 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.runConnections() + } + + // Make sure both server and client are running + try await Task.sleep(for: .milliseconds(100), tolerance: .zero) + try await body(client, server) + client.beginGracefulShutdown() + server.beginGracefulShutdown() + } + } +} diff --git a/Tests/GRPCCoreTests/GRPCServerTests.swift b/Tests/GRPCCoreTests/GRPCServerTests.swift index d8771d81e..9e35bded6 100644 --- a/Tests/GRPCCoreTests/GRPCServerTests.swift +++ b/Tests/GRPCCoreTests/GRPCServerTests.swift @@ -16,34 +16,31 @@ import GRPCCore import GRPCInProcessTransport +import Testing import XCTest -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +@available(gRPCSwift 2.0, *) final class GRPCServerTests: XCTestCase { func withInProcessClientConnectedToServer( services: [any RegistrableRPCService], - interceptors: [any ServerInterceptor] = [], - _ body: (InProcessClientTransport, GRPCServer) async throws -> Void + interceptorPipeline: [ConditionalInterceptor] = [], + _ body: (InProcessTransport.Client, GRPCServer) async throws -> Void ) async throws { - let inProcess = InProcessTransport.makePair() - let server = GRPCServer( + let inProcess = InProcessTransport() + + try await withGRPCServer( transport: inProcess.server, services: services, - interceptors: interceptors - ) + interceptorPipeline: interceptorPipeline + ) { server in + try await withThrowingTaskGroup(of: Void.self) { group in + group.addTask { + try await inProcess.client.connect() + } - try await withThrowingTaskGroup(of: Void.self) { group in - group.addTask { - try await server.serve() + try await body(inProcess.client, server) + inProcess.client.beginGracefulShutdown() } - - group.addTask { - try await inProcess.client.connect() - } - - try await body(inProcess.client, server) - inProcess.client.beginGracefulShutdown() - server.beginGracefulShutdown() } } @@ -52,7 +49,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() @@ -79,7 +76,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])) @@ -110,7 +107,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() @@ -139,7 +136,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])) @@ -168,9 +165,9 @@ 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 + ) { stream, _ in try await stream.outbound.write(.metadata([:])) await stream.outbound.finish() @@ -191,7 +188,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() @@ -220,16 +217,16 @@ 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( descriptor: BinaryEcho.Methods.get, options: .defaults - ) { stream in + ) { stream, _ in try await stream.outbound.write(.metadata([:])) await stream.outbound.finish() @@ -249,12 +246,12 @@ 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"), + descriptor: MethodDescriptor(fullyQualifiedService: "not", method: "implemented"), options: .defaults - ) { stream in + ) { stream, _ in try await stream.outbound.write(.metadata([:])) await stream.outbound.finish() @@ -281,7 +278,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 @@ -295,7 +292,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. @@ -317,7 +314,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 +335,10 @@ final class GRPCServerTests: XCTestCase { } func testTestRunStoppedServer() async throws { - let server = GRPCServer(transport: InProcessServerTransport(), services: []) + let server = GRPCServer( + transport: InProcessTransport.Server(peer: "in-process:1234"), + services: [] + ) // Run the server. let task = Task { try await server.serve() } task.cancel() @@ -361,11 +361,11 @@ 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 - ) { stream in + ) { stream, _ in try await stream.outbound.write(.metadata([:])) try await stream.outbound.write(.message([0])) await stream.outbound.finish() @@ -375,3 +375,249 @@ 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() + 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") + @available(gRPCSwift 2.0, *) + 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) + } + } + + @available(gRPCSwift 2.0, *) + func withInProcessClientConnectedToServer( + services: [any RegistrableRPCService], + interceptorPipeline: [ConditionalInterceptor] = [], + _ 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() + } + } + + @available(gRPCSwift 2.0, *) + 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))'") + } + } + + @available(gRPCSwift 2.0, *) + func assertMessage( + _ part: RPCResponsePart?, + messageHandler: (Bytes) -> Void = { _ in } + ) { + switch part { + case .some(.message(let message)): + messageHandler(message) + default: + Issue.record("Expected '.message' but found '\(String(describing: part))'") + } + } + + @available(gRPCSwift 2.0, *) + 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/Internal/Metadata+GRPCTests.swift b/Tests/GRPCCoreTests/Internal/Metadata+GRPCTests.swift index 25ded0048..b4b59bcef 100644 --- a/Tests/GRPCCoreTests/Internal/Metadata+GRPCTests.swift +++ b/Tests/GRPCCoreTests/Internal/Metadata+GRPCTests.swift @@ -18,7 +18,7 @@ import XCTest @testable import GRPCCore -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@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 58ddcde4f..1f9cf7bdf 100644 --- a/Tests/GRPCCoreTests/Internal/MethodConfigsTests.swift +++ b/Tests/GRPCCoreTests/Internal/MethodConfigsTests.swift @@ -16,7 +16,7 @@ import GRPCCore import XCTest -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@available(gRPCSwift 2.0, *) final class MethodConfigsTests: XCTestCase { func testGetConfigurationForKnownMethod() async throws { let policy = HedgingPolicy( @@ -27,7 +27,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), @@ -50,7 +50,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), @@ -61,7 +61,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) } @@ -74,7 +74,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), @@ -85,7 +85,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/Internal/Result+CatchingTests.swift b/Tests/GRPCCoreTests/Internal/Result+CatchingTests.swift index aee39daf4..644bc72dd 100644 --- a/Tests/GRPCCoreTests/Internal/Result+CatchingTests.swift +++ b/Tests/GRPCCoreTests/Internal/Result+CatchingTests.swift @@ -18,11 +18,11 @@ import XCTest @testable import GRPCCore -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +@available(gRPCSwift 2.0, *) 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/GRPCCoreTests/MetadataTests.swift b/Tests/GRPCCoreTests/MetadataTests.swift index f0b29df04..647a34179 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==", @@ -212,7 +221,20 @@ 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() { let metadata: Metadata = [ "key1": "value1", @@ -228,28 +250,137 @@ 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 { + @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 { + var otherMetadata = self.otherMetadata + otherMetadata.addString("value1-2", forKey: "key1") + var metadata = metadata + metadata.add(contentsOf: otherMetadata) + + #expect( + metadata == [ + "key1": "value1-1", + "key2": "value2", + "key3": "value3", + "key4": "value4", + "key5": "value5", + "key1": "value1-2", + ] + ) + } + + @Test("Where key is already present with same value") + @available(gRPCSwift 2.0, *) + mutating func mergeWhereKeyIsAlreadyPresentWithSameValue() async throws { + var otherMetadata = otherMetadata + otherMetadata.addString("value1-1", forKey: "key1") + var metadata = metadata + metadata.add(contentsOf: otherMetadata) + + #expect( + metadata == [ + "key1": "value1-1", + "key2": "value2", + "key3": "value3", + "key4": "value4", + "key5": "value5", + "key1": "value1-1", + ] + ) + } + + @Test("Where key is not already present") + @available(gRPCSwift 2.0, *) + mutating func mergeWhereKeyIsNotAlreadyPresent() async throws { + var metadata = self.metadata + metadata.add(contentsOf: self.otherMetadata) + + #expect( + metadata == [ + "key1": "value1-1", + "key2": "value2", + "key3": "value3", + "key4": "value4", + "key5": "value5", + ] + ) + } + } + + @Suite("Description") + struct Description { + @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 { + 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") + } + } } } } diff --git a/Tests/GRPCCoreTests/MethodDescriptorTests.swift b/Tests/GRPCCoreTests/MethodDescriptorTests.swift index cf4568898..12329ffc0 100644 --- a/Tests/GRPCCoreTests/MethodDescriptorTests.swift +++ b/Tests/GRPCCoreTests/MethodDescriptorTests.swift @@ -14,13 +14,27 @@ * limitations under the License. */ import GRPCCore -import XCTest +import Testing -final class MethodDescriptorTests: XCTestCase { +@Suite +struct MethodDescriptorTests { + @Test("Fully qualified name") + @available(gRPCSwift 2.0, *) 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") + @available(gRPCSwift 2.0, *) + func description() { + let descriptor = MethodDescriptor( + service: ServiceDescriptor(fullyQualifiedService: "foo.Foo"), + method: "Bar" + ) + + #expect(String(describing: descriptor) == "foo.Foo/Bar") } } diff --git a/Tests/GRPCCoreTests/RPCErrorTests.swift b/Tests/GRPCCoreTests/RPCErrorTests.swift index dc65122b0..7c9968423 100644 --- a/Tests/GRPCCoreTests/RPCErrorTests.swift +++ b/Tests/GRPCCoreTests/RPCErrorTests.swift @@ -14,114 +14,234 @@ * 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") + @available(gRPCSwift 2.0, *) 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") + @available(gRPCSwift 2.0, *) 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), + ] + ) + @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() { - 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"]) + ) + + #expect( + RPCError(code: .cancelled, message: "") + != RPCError(code: .cancelled, message: "message") ) - XCTAssertNotEqual( - RPCError(code: .cancelled, message: ""), + #expect( 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"]) + ) + } + + @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), + ] + ) + @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) + 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 ) - XCTAssertNotEqual( - RPCError(code: .cancelled, message: "message"), - RPCError(code: .unknown, message: "message") + 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) + } - XCTAssertNotEqual( - RPCError(code: .cancelled, message: "message", metadata: ["foo": "bar"]), - RPCError(code: .cancelled, message: "message", metadata: ["foo": "baz"]) + @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) + 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) + } + + @Test("Convert type to RPCError") + @available(gRPCSwift 2.0, *) + 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") + @available(gRPCSwift 2.0, *) + 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) } - func testStatusCodeRawValues() { - for (code, expected) in Self.statusCodeRawValue { - XCTAssertEqual(code.rawValue, expected, "\(code) had unexpected raw value") + @Test("Convert error to RPCError with defaults") + @available(gRPCSwift 2.0, *) + 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) } } diff --git a/Tests/GRPCCoreTests/RPCPartsTests.swift b/Tests/GRPCCoreTests/RPCPartsTests.swift index e950a8e97..605821fb0 100644 --- a/Tests/GRPCCoreTests/RPCPartsTests.swift +++ b/Tests/GRPCCoreTests/RPCPartsTests.swift @@ -16,9 +16,10 @@ import GRPCCore import XCTest +@available(gRPCSwift 2.0, *) 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/RuntimeErrorTests.swift b/Tests/GRPCCoreTests/RuntimeErrorTests.swift index 2d30f96f0..9881a60e5 100644 --- a/Tests/GRPCCoreTests/RuntimeErrorTests.swift +++ b/Tests/GRPCCoreTests/RuntimeErrorTests.swift @@ -16,7 +16,7 @@ import GRPCCore import XCTest -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +@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 0adfe524a..20c5897cd 100644 --- a/Tests/GRPCCoreTests/ServiceDescriptorTests.swift +++ b/Tests/GRPCCoreTests/ServiceDescriptorTests.swift @@ -14,13 +14,33 @@ * 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"), + ("..", ".", ""), + (".", "", ""), + ("", "", ""), + ] + ) + @available(gRPCSwift 2.0, *) + 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") + @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 936ff8e41..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,7 +72,28 @@ struct StatusTests { } @Test("Fits in existential container") + @available(gRPCSwift 2.0, *) 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")), + ] + ) + @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 648e935e9..a612649cf 100644 --- a/Tests/GRPCCoreTests/Streaming/Internal/AsyncSequenceOfOne.swift +++ b/Tests/GRPCCoreTests/Streaming/Internal/AsyncSequenceOfOne.swift @@ -18,7 +18,7 @@ import XCTest @testable import GRPCCore -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +@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 6195977c6..969025063 100644 --- a/Tests/GRPCCoreTests/Streaming/Internal/BroadcastAsyncSequenceTests.swift +++ b/Tests/GRPCCoreTests/Streaming/Internal/BroadcastAsyncSequenceTests.swift @@ -18,7 +18,7 @@ import XCTest @testable import GRPCCore -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +@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/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..dc38b25d9 100644 --- a/Tests/GRPCCoreTests/Test Utilities/AtomicCounter.swift +++ b/Tests/GRPCCoreTests/Test Utilities/AtomicCounter.swift @@ -16,7 +16,7 @@ import Synchronization -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +@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 a45d64fd5..3c46a35d5 100644 --- a/Tests/GRPCCoreTests/Test Utilities/Call/Client/ClientInterceptors.swift +++ b/Tests/GRPCCoreTests/Test Utilities/Call/Client/ClientInterceptors.swift @@ -16,19 +16,19 @@ import GRPCCore -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +@available(gRPCSwift 2.0, *) 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) } } -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +@available(gRPCSwift 2.0, *) extension ClientInterceptor where Self == RequestCountingClientInterceptor { static func requestCounter(_ counter: AtomicCounter) -> Self { return RequestCountingClientInterceptor(counter: counter) @@ -36,36 +36,43 @@ 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, *) +@available(gRPCSwift 2.0, *) 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( - request: ClientRequest.Stream, + request: StreamingClientRequest, context: ClientContext, next: ( - ClientRequest.Stream, + StreamingClientRequest, ClientContext - ) async throws -> ClientResponse.Stream - ) async throws -> ClientResponse.Stream { - if self.throw { - throw self.error - } else { - return ClientResponse.Stream(error: self.error) + ) async throws -> StreamingClientResponse + ) async throws -> StreamingClientResponse { + switch self.mode { + case .throw(let error): + throw error + case .reject(let error): + return StreamingClientResponse(error: error) } } } -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +@available(gRPCSwift 2.0, *) struct RequestCountingClientInterceptor: ClientInterceptor { /// The number of requests made. let counter: AtomicCounter @@ -75,13 +82,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..5918102db 100644 --- a/Tests/GRPCCoreTests/Test Utilities/Call/Server/ServerInterceptors.swift +++ b/Tests/GRPCCoreTests/Test Utilities/Call/Server/ServerInterceptors.swift @@ -16,55 +16,62 @@ import GRPCCore -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +@available(gRPCSwift 2.0, *) 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 { - return RejectAllServerInterceptor(error: error, throw: true) + static func throwError(_ error: any Error) -> Self { + RejectAllServerInterceptor(throw: error) } } -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +@available(gRPCSwift 2.0, *) extension ServerInterceptor where Self == RequestCountingServerInterceptor { static func requestCounter(_ counter: AtomicCounter) -> Self { - return RequestCountingServerInterceptor(counter: counter) + RequestCountingServerInterceptor(counter: counter) } } /// Rejects all RPCs with the provided error. -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +@available(gRPCSwift 2.0, *) 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( - request: ServerRequest.Stream, + request: StreamingServerRequest, context: ServerContext, next: @Sendable ( - ServerRequest.Stream, + StreamingServerRequest, ServerContext - ) async throws -> ServerResponse.Stream - ) async throws -> ServerResponse.Stream { - if self.throw { - throw self.error - } else { - return ServerResponse.Stream(error: self.error) + ) async throws -> StreamingServerResponse + ) async throws -> StreamingServerResponse { + switch self.mode { + case .throw(let error): + throw error + case .reject(let error): + return StreamingServerResponse(error: error) } } } -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +@available(gRPCSwift 2.0, *) struct RequestCountingServerInterceptor: ServerInterceptor { /// The number of requests made. let counter: AtomicCounter @@ -74,13 +81,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/Coding+Identity.swift b/Tests/GRPCCoreTests/Test Utilities/Coding+Identity.swift index 335426fad..cc27182b5 100644 --- a/Tests/GRPCCoreTests/Test Utilities/Coding+Identity.swift +++ b/Tests/GRPCCoreTests/Test Utilities/Coding+Identity.swift @@ -15,14 +15,18 @@ */ import GRPCCore +@available(gRPCSwift 2.0, *) struct IdentitySerializer: MessageSerializer { - func serialize(_ message: [UInt8]) throws -> [UInt8] { - return message + func serialize(_ message: [UInt8]) throws -> Bytes { + return Bytes(message) } } +@available(gRPCSwift 2.0, *) 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..bb734479f 100644 --- a/Tests/GRPCCoreTests/Test Utilities/Coding+JSON.swift +++ b/Tests/GRPCCoreTests/Test Utilities/Coding+JSON.swift @@ -13,28 +13,33 @@ * 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 +@available(gRPCSwift 2.0, *) 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)") } } } +@available(gRPCSwift 2.0, *) 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/RPCAsyncSequence+Utilities.swift b/Tests/GRPCCoreTests/Test Utilities/RPCAsyncSequence+Utilities.swift index b996e3d27..64c7c85dd 100644 --- a/Tests/GRPCCoreTests/Test Utilities/RPCAsyncSequence+Utilities.swift +++ b/Tests/GRPCCoreTests/Test Utilities/RPCAsyncSequence+Utilities.swift @@ -15,7 +15,7 @@ */ import GRPCCore -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +@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 923ab267d..d4426c4e9 100644 --- a/Tests/GRPCCoreTests/Test Utilities/RPCWriter+Utilities.swift +++ b/Tests/GRPCCoreTests/Test Utilities/RPCWriter+Utilities.swift @@ -16,7 +16,7 @@ import GRPCCore import XCTest -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +@available(gRPCSwift 2.0, *) extension RPCWriter { /// Returns a writer which calls `XCTFail(_:)` on every write. static func failTestOnWrite(elementType: Element.Type = Element.self) -> Self { @@ -29,7 +29,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 +39,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 e5438c550..1bd5bbbc4 100644 --- a/Tests/GRPCCoreTests/Test Utilities/Services/BinaryEcho.swift +++ b/Tests/GRPCCoreTests/Test Utilities/Services/BinaryEcho.swift @@ -14,27 +14,28 @@ * limitations under the License. */ import GRPCCore -import XCTest -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +@available(gRPCSwift 2.0, *) struct BinaryEcho: RegistrableRPCService { + static let serviceDescriptor = ServiceDescriptor(package: "echo", service: "Echo") + 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 +44,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) } @@ -53,7 +54,7 @@ struct BinaryEcho: RegistrableRPCService { } } - func registerMethods(with router: inout RPCRouter) { + func registerMethods(with router: inout RPCRouter) { let serializer = IdentitySerializer() let deserializer = IdentityDeserializer() @@ -62,9 +63,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 +74,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 +82,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 } @@ -97,9 +98,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( + fullyQualifiedService: BinaryEcho.serviceDescriptor.fullyQualifiedService, + method: "Get" + ) + static let collect = MethodDescriptor( + fullyQualifiedService: BinaryEcho.serviceDescriptor.fullyQualifiedService, + method: "Collect" + ) + static let expand = MethodDescriptor( + fullyQualifiedService: BinaryEcho.serviceDescriptor.fullyQualifiedService, + method: "Expand" + ) + static let update = MethodDescriptor( + fullyQualifiedService: 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..12777517f --- /dev/null +++ b/Tests/GRPCCoreTests/Test Utilities/Services/HelloWorld.swift @@ -0,0 +1,51 @@ +/* + * Copyright 2024, gRPC Authors All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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(gRPCSwift 2.0, *) +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( + fullyQualifiedService: HelloWorld.serviceDescriptor.fullyQualifiedService, + method: "SayHello" + ) + } +} diff --git a/Tests/GRPCCoreTests/Test Utilities/Transport/AnyTransport.swift b/Tests/GRPCCoreTests/Test Utilities/Transport/AnyTransport.swift index eb8208b72..2e97f8f0b 100644 --- a/Tests/GRPCCoreTests/Test Utilities/Transport/AnyTransport.swift +++ b/Tests/GRPCCoreTests/Test Utilities/Transport/AnyTransport.swift @@ -15,28 +15,26 @@ */ @testable import GRPCCore -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +@available(gRPCSwift 2.0, *) struct AnyClientTransport: ClientTransport, Sendable { - typealias Inbound = RPCAsyncSequence - typealias Outbound = RPCWriter.Closable + typealias Bytes = [UInt8] private let _retryThrottle: @Sendable () -> RetryThrottle? private let _withStream: @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 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 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) } } @@ -68,7 +66,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 @@ -81,10 +79,9 @@ struct AnyClientTransport: ClientTransport, Sendable { } } -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +@available(gRPCSwift 2.0, *) struct AnyServerTransport: ServerTransport, Sendable { - typealias Inbound = RPCAsyncSequence - typealias Outbound = RPCWriter.Closable + typealias Bytes = [UInt8] private let _listen: @Sendable ( @@ -95,7 +92,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 835c81b79..5b1ef428f 100644 --- a/Tests/GRPCCoreTests/Test Utilities/Transport/StreamCountingTransport.swift +++ b/Tests/GRPCCoreTests/Test Utilities/Transport/StreamCountingTransport.swift @@ -16,10 +16,9 @@ @testable import GRPCCore -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +@available(gRPCSwift 2.0, *) struct StreamCountingClientTransport: ClientTransport, Sendable { - typealias Inbound = RPCAsyncSequence - typealias Outbound = RPCWriter.Closable + typealias Bytes = [UInt8] private let transport: AnyClientTransport private let _streamsOpened: AtomicCounter @@ -33,8 +32,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() @@ -55,15 +53,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() @@ -78,10 +76,9 @@ struct StreamCountingClientTransport: ClientTransport, Sendable { } } -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +@available(gRPCSwift 2.0, *) struct StreamCountingServerTransport: ServerTransport, Sendable { - typealias Inbound = RPCAsyncSequence - typealias Outbound = RPCWriter.Closable + typealias Bytes = [UInt8] private let transport: AnyServerTransport private let _acceptedStreams: AtomicCounter @@ -90,7 +87,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 804be7d52..c635dc249 100644 --- a/Tests/GRPCCoreTests/Test Utilities/Transport/ThrowingTransport.swift +++ b/Tests/GRPCCoreTests/Test Utilities/Transport/ThrowingTransport.swift @@ -15,10 +15,9 @@ */ @testable import GRPCCore -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +@available(gRPCSwift 2.0, *) struct ThrowOnStreamCreationTransport: ClientTransport { - typealias Inbound = RPCAsyncSequence - typealias Outbound = RPCWriter.Closable + typealias Bytes = [UInt8] private let code: RPCError.Code @@ -45,14 +44,16 @@ 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: "") } } -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +@available(gRPCSwift 2.0, *) struct ThrowOnRunServerTransport: ServerTransport { + typealias Bytes = [UInt8] + func listen( streamHandler: ( _ stream: RPCStream, @@ -70,8 +71,10 @@ struct ThrowOnRunServerTransport: ServerTransport { } } -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +@available(gRPCSwift 2.0, *) 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 a6bb1ee50..0e4b5e960 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, @@ -67,6 +65,7 @@ func XCTAssertThrowsErrorAsync( } } +@available(gRPCSwift 2.0, *) func XCTAssertThrowsRPCError( _ expression: @autoclosure () throws -> T, _ errorHandler: (RPCError) -> Void @@ -80,7 +79,7 @@ func XCTAssertThrowsRPCError( } } -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +@available(gRPCSwift 2.0, *) func XCTAssertThrowsRPCErrorAsync( _ expression: () async throws -> T, errorHandler: (RPCError) -> Void @@ -95,9 +94,9 @@ func XCTAssertThrowsRPCErrorAsync( } } -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +@available(gRPCSwift 2.0, *) func XCTAssertRejected( - _ response: ClientResponse.Stream, + _ response: StreamingClientResponse, errorHandler: (RPCError) -> Void ) { switch response.accepted { @@ -108,8 +107,9 @@ func XCTAssertRejected( } } +@available(gRPCSwift 2.0, *) func XCTAssertRejected( - _ response: ClientResponse.Single, + _ response: ClientResponse, errorHandler: (RPCError) -> Void ) { switch response.accepted { @@ -120,8 +120,9 @@ func XCTAssertRejected( } } -func XCTAssertMetadata( - _ part: RPCResponsePart?, +@available(gRPCSwift 2.0, *) +func XCTAssertMetadata( + _ part: RPCResponsePart?, metadataHandler: (Metadata) -> Void = { _ in } ) { switch part { @@ -132,9 +133,9 @@ func XCTAssertMetadata( } } -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -func XCTAssertMetadata( - _ part: RPCRequestPart?, +@available(gRPCSwift 2.0, *) +func XCTAssertMetadata( + _ part: RPCRequestPart?, metadataHandler: (Metadata) async throws -> Void = { _ in } ) async throws { switch part { @@ -145,9 +146,10 @@ func XCTAssertMetadata( } } -func XCTAssertMessage( - _ part: RPCResponsePart?, - messageHandler: ([UInt8]) -> Void = { _ in } +@available(gRPCSwift 2.0, *) +func XCTAssertMessage( + _ part: RPCResponsePart?, + messageHandler: (Bytes) -> Void = { _ in } ) { switch part { case .some(.message(let message)): @@ -157,10 +159,10 @@ func XCTAssertMessage( } } -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -func XCTAssertMessage( - _ part: RPCRequestPart?, - messageHandler: ([UInt8]) async throws -> Void = { _ in } +@available(gRPCSwift 2.0, *) +func XCTAssertMessage( + _ part: RPCRequestPart?, + messageHandler: (Bytes) async throws -> Void = { _ in } ) async throws { switch part { case .some(.message(let message)): @@ -170,8 +172,9 @@ func XCTAssertMessage( } } -func XCTAssertStatus( - _ part: RPCResponsePart?, +@available(gRPCSwift 2.0, *) +func XCTAssertStatus( + _ part: RPCResponsePart?, statusHandler: (Status, Metadata) -> Void = { _, _ in } ) { switch part { 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 ee2ad3742..123ca966e 100644 --- a/Tests/GRPCCoreTests/Transport/RetryThrottleTests.swift +++ b/Tests/GRPCCoreTests/Transport/RetryThrottleTests.swift @@ -18,7 +18,7 @@ import XCTest @testable import GRPCCore -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +@available(gRPCSwift 2.0, *) final class RetryThrottleTests: XCTestCase { func testThrottleOnInit() { let throttle = RetryThrottle(maxTokens: 10, tokenRatio: 0.1) 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/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/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/GRPCInProcessTransportTests/ClientServerWithMethods.swift b/Tests/GRPCInProcessTransportTests/ClientServerWithMethods.swift new file mode 100644 index 000000000..16391c1d9 --- /dev/null +++ b/Tests/GRPCInProcessTransportTests/ClientServerWithMethods.swift @@ -0,0 +1,58 @@ +/* + * Copyright 2024, gRPC Authors All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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") + @available(gRPCSwift 2.0, *) + func actorIsolation() async throws { + let testActor = TestActor() + #expect(await !testActor.hasRun) + try await testActor.run() + #expect(await testActor.hasRun) + } +} + +@available(gRPCSwift 2.0, *) +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) + } + } + } + } +} diff --git a/Tests/GRPCInProcessTransportTests/InProcessClientTransportTests.swift b/Tests/GRPCInProcessTransportTests/InProcessClientTransportTests.swift index d33b2774d..9dd66feb9 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. @@ -18,7 +18,7 @@ import GRPCCore import GRPCInProcessTransport import XCTest -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +@available(gRPCSwift 2.0, *) final class InProcessClientTransportTests: XCTestCase { struct FailTest: Error {} @@ -63,7 +63,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() @@ -98,7 +98,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() @@ -111,10 +111,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()`. @@ -125,7 +122,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() } @@ -139,17 +136,14 @@ 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) } } func testOpenStreamSuccessfullyAndThenClose() async throws { - let server = InProcessServerTransport() + let server = InProcessTransport.Server(peer: "in-process:1234") let client = makeClient(server: server) try await withThrowingTaskGroup(of: Void.self) { group in @@ -158,10 +152,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) } @@ -181,7 +172,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() } @@ -208,12 +199,14 @@ final class InProcessClientTransportTests: XCTestCase { ] ) - var client = InProcessClientTransport( - server: InProcessServerTransport(), - serviceConfig: serviceConfig + let peer = "in-process:1234" + var client = InProcessTransport.Client( + server: InProcessTransport.Server(peer: peer), + serviceConfig: serviceConfig, + peer: peer ) - let firstDescriptor = MethodDescriptor(service: "test", method: "first") + let firstDescriptor = MethodDescriptor(fullyQualifiedService: "test", method: "first") XCTAssertEqual( client.config(forMethod: firstDescriptor), serviceConfig.methodConfig.first @@ -232,12 +225,13 @@ final class InProcessClientTransportTests: XCTestCase { executionPolicy: .retry(retryPolicy) ) serviceConfig.methodConfig.append(overrideConfiguration) - client = InProcessClientTransport( - server: InProcessServerTransport(), - serviceConfig: serviceConfig + client = InProcessTransport.Client( + server: InProcessTransport.Server(peer: peer), + serviceConfig: serviceConfig, + peer: peer ) - let secondDescriptor = MethodDescriptor(service: "test", method: "second") + let secondDescriptor = MethodDescriptor(fullyQualifiedService: "test", method: "second") XCTAssertEqual( client.config(forMethod: firstDescriptor), serviceConfig.methodConfig.first @@ -249,7 +243,7 @@ final class InProcessClientTransportTests: XCTestCase { } func testOpenMultipleStreamsThenClose() async throws { - let server = InProcessServerTransport() + let server = InProcessTransport.Server(peer: "in-process:1234") let client = makeClient(server: server) try await withThrowingTaskGroup(of: Void.self) { group in @@ -258,25 +252,19 @@ final class InProcessClientTransportTests: XCTestCase { } group.addTask { - try await client.withStream( - descriptor: .init(service: "test", method: "test"), - options: .defaults - ) { stream in - try await Task.sleep(for: .milliseconds(100)) + try await client.withStream(descriptor: .testTest, options: .defaults) { stream, _ in + try await Task.sleep(for: .milliseconds(100), tolerance: .zero) } } group.addTask { - try await client.withStream( - descriptor: .init(service: "test", method: "test"), - options: .defaults - ) { stream in - try await Task.sleep(for: .milliseconds(100)) + try await client.withStream(descriptor: .testTest, options: .defaults) { stream, _ in + 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() } @@ -284,9 +272,10 @@ final class InProcessClientTransportTests: XCTestCase { } } + @available(gRPCSwift 2.0, *) func makeClient( - server: InProcessServerTransport = InProcessServerTransport() - ) -> InProcessClientTransport { + server: InProcessTransport.Server = InProcessTransport.Server(peer: "in-process:1234") + ) -> InProcessTransport.Client { let defaultPolicy = RetryPolicy( maxAttempts: 10, initialBackoff: .seconds(1), @@ -304,9 +293,15 @@ final class InProcessClientTransportTests: XCTestCase { ] ) - return InProcessClientTransport( + return InProcessTransport.Client( server: server, - serviceConfig: serviceConfig + serviceConfig: serviceConfig, + peer: server.peer ) } } + +@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 7cd8fed20..7d9a70093 100644 --- a/Tests/GRPCInProcessTransportTests/InProcessServerTransportTests.swift +++ b/Tests/GRPCInProcessTransportTests/InProcessServerTransportTests.swift @@ -19,17 +19,17 @@ import XCTest @testable import GRPCCore @testable import GRPCInProcessTransport -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +@available(gRPCSwift 2.0, *) final class InProcessServerTransportTests: XCTestCase { func testStartListening() async throws { - let transport = InProcessServerTransport() + 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: .init(service: "testService", method: "testMethod"), + descriptor: .testTest, inbound: RPCAsyncSequence( wrapping: AsyncThrowingStream { $0.yield(.message([42])) @@ -54,13 +54,14 @@ final class InProcessServerTransportTests: XCTestCase { } func testStopListening() async throws { - let transport = InProcessServerTransport() + 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: .init(service: "testService1", method: "testMethod1"), + descriptor: .testTest, inbound: RPCAsyncSequence( wrapping: AsyncThrowingStream { $0.yield(.message([42])) @@ -80,11 +81,14 @@ 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: .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 new file mode 100644 index 000000000..40a2e0b46 --- /dev/null +++ b/Tests/GRPCInProcessTransportTests/InProcessTransportTests.swift @@ -0,0 +1,206 @@ +/* + * 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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"] + + @available(gRPCSwift 2.0, *) + 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.runConnections() + } + + try await execute(server, client) + } + } + + @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( + 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 runConnections() method returns. + client.beginGracefulShutdown() + } + } + + @Test("Peer info") + @available(gRPCSwift 2.0, *) + 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: JSONDeserializer(), + options: .defaults + ) { + try $0.message + } + + #expect(peerInfo.local == peerInfo.remote) + } + } +} + +@available(gRPCSwift 2.0, *) +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 peerInfo( + request: ServerRequest, + context: ServerContext + ) async throws -> ServerResponse { + let peerInfo = PeerInfo(local: context.localPeer, remote: context.remotePeer) + return ServerResponse(message: peerInfo) + } + + 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) + } + ) + + router.registerHandler( + forMethod: .peerInfo, + deserializer: VoidDeserializer(), + serializer: JSONSerializer(), + handler: { + let response = try await self.peerInfo( + request: ServerRequest(stream: $0), + context: $1 + ) + return StreamingServerResponse(single: response) + } + ) + } +} + +@available(gRPCSwift 2.0, *) +extension MethodDescriptor { + fileprivate static let testCancellation = Self( + fullyQualifiedService: "test", + method: "cancellation" + ) + + fileprivate static let peerInfo = Self( + fullyQualifiedService: "test", + method: "peerInfo" + ) +} + +private struct PeerInfo: Codable { + var local: String + 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 { + String(decoding: $0, as: UTF8.self) + } + } +} + +@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 new file mode 100644 index 000000000..ed44053c9 --- /dev/null +++ b/Tests/GRPCInProcessTransportTests/Test Utilities/JSONSerializing.swift @@ -0,0 +1,47 @@ +/* + * 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 + +@available(gRPCSwift 2.0, *) +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)") + } + } +} + +@available(gRPCSwift 2.0, *) +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)") + } + } +} 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, 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/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/dev/build-examples.sh b/dev/build-examples.sh new file mode 100755 index 000000000..4144dab2c --- /dev/null +++ b/dev/build-examples.sh @@ -0,0 +1,38 @@ +#!/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 new file mode 100755 index 000000000..3094c38b4 --- /dev/null +++ b/dev/check-generated-code.sh @@ -0,0 +1,31 @@ +#!/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/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 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 61% rename from scripts/format.sh rename to dev/format.sh index f9ded9164..1201d6861 100755 --- a/scripts/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 @@ -60,8 +59,9 @@ if "$lint"; then --parallel --recursive --strict \ "${repo}/Sources" \ "${repo}/Tests" \ - "${repo}/Plugins" \ - "${repo}/Performance/Benchmarks/Benchmarks/GRPCSwiftBenchmark" \ + "${repo}/Examples" \ + "${repo}/IntegrationTests/Benchmarks/Benchmarks/GRPCSwiftBenchmark" \ + "${repo}/dev" \ && SWIFT_FORMAT_RC=$? || SWIFT_FORMAT_RC=$? if [[ "${SWIFT_FORMAT_RC}" -ne 0 ]]; then @@ -69,9 +69,8 @@ if "$lint"; then To fix, run the following command: - % $THIS_SCRIPT -f - " - exit "${SWIFT_FORMAT_RC}" + % $here/format.sh -f + " "${SWIFT_FORMAT_RC}" fi log "Ran swift format lint with no errors." @@ -80,8 +79,9 @@ elif "$format"; then --parallel --recursive --in-place \ "${repo}/Sources" \ "${repo}/Tests" \ - "${repo}/Plugins" \ - "${repo}/Performance/Benchmarks/Benchmarks/GRPCSwiftBenchmark" \ + "${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/Tests/GRPCTests/SampleCertificate+Assertions.swift b/dev/grpc-dev-tool/Package.swift similarity index 50% rename from Tests/GRPCTests/SampleCertificate+Assertions.swift rename to dev/grpc-dev-tool/Package.swift index b1a03e0d1..8f8d3f892 100644 --- a/Tests/GRPCTests/SampleCertificate+Assertions.swift +++ b/dev/grpc-dev-tool/Package.swift @@ -1,5 +1,6 @@ +// swift-tools-version:6.0 /* - * Copyright 2019, gRPC Authors All rights reserved. + * 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. @@ -13,20 +14,23 @@ * 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 +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"), + ] ) - } -} -#endif // canImport(NIOSSL) + ] +) diff --git a/Sources/GRPC/DebugOnly.swift b/dev/grpc-dev-tool/Sources/grpc-dev-tool/GRPCDevTool.swift similarity index 69% rename from Sources/GRPC/DebugOnly.swift rename to dev/grpc-dev-tool/Sources/grpc-dev-tool/GRPCDevTool.swift index c30546970..5cf08ea27 100644 --- a/Sources/GRPC/DebugOnly.swift +++ b/dev/grpc-dev-tool/Sources/grpc-dev-tool/GRPCDevTool.swift @@ -1,5 +1,5 @@ /* - * Copyright 2021, gRPC Authors All rights reserved. + * 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. @@ -14,11 +14,12 @@ * limitations under the License. */ -internal func debugOnly(_ body: () -> Void) { - assert( - { - body() - return true - }() +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 + } +} diff --git a/scripts/license-check.sh b/dev/license-check.sh similarity index 74% rename from scripts/license-check.sh rename to dev/license-check.sh index e006af8bc..be92bcf85 100755 --- a/scripts/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 @@ -93,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}') @@ -105,14 +100,8 @@ 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/*') + ! -path '.*/.build/*') } errors=0 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 99% rename from Protos/examples/echo/echo.proto rename to dev/protos/examples/echo/echo.proto index 7de1534ac..cfba875db 100644 --- a/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/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/dev/protos/fetch.sh b/dev/protos/fetch.sh new file mode 100755 index 000000000..2032a2408 --- /dev/null +++ b/dev/protos/fetch.sh @@ -0,0 +1,44 @@ +#!/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/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 "$checkouts/grpc-proto/grpc/examples/helloworld.proto" "$upstream/grpc/examples/helloworld.proto" + +# 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/dev/protos/generate.sh b/dev/protos/generate.sh new file mode 100755 index 000000000..dec5de14c --- /dev/null +++ b/dev/protos/generate.sh @@ -0,0 +1,94 @@ +#!/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 -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 + +# 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" "$@" +} + +#- 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 +} + +#------------------------------------------------------------------------------ + +# Tests +generate_service_config_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/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/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 99% rename from Protos/upstream/grpc/lookup/v1/rls_config.proto rename to dev/protos/upstream/grpc/lookup/v1/rls_config.proto index 9d2b6c54c..9762be752 100644 --- a/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/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/scripts/sanity.sh b/dev/soundness.sh similarity index 50% rename from scripts/sanity.sh rename to dev/soundness.sh index de039b514..8c9f16e07 100755 --- a/scripts/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 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 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/check-generated-code.sh b/scripts/check-generated-code.sh deleted file mode 100755 index 3444d9d88..000000000 --- a/scripts/check-generated-code.sh +++ /dev/null @@ -1,32 +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 -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/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"