diff --git a/.github/workflows/TagBot.yml b/.github/workflows/TagBot.yml index 778c06f..f49313b 100644 --- a/.github/workflows/TagBot.yml +++ b/.github/workflows/TagBot.yml @@ -12,3 +12,4 @@ jobs: - uses: JuliaRegistries/TagBot@v1 with: token: ${{ secrets.GITHUB_TOKEN }} + ssh: ${{ secrets.DOCUMENTER_KEY }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2efebc9..73e3ac3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,73 +1,59 @@ name: CI + on: push: - branches: [main] - tags: ["*"] + branches: [ "master" ] pull_request: + branches: [ "master" ] + +env: + GRPC_TEST_SERVER_HOST: "localhost" + GRPC_TEST_SERVER_PORT: 8001 + jobs: test: name: Julia ${{ matrix.version }} - ${{ matrix.os }} - ${{ matrix.arch }} - ${{ github.event_name }} runs-on: ${{ matrix.os }} - continue-on-error: ${{ matrix.experimental }} strategy: fail-fast: false matrix: version: - - '1.6' - - '1' # automatically expands to the latest stable 1.x release of Julia + - '1.10' + - '1.12' + - 'nightly' os: - ubuntu-latest arch: - x64 - - x86 - experimental: [false] - include: - # allow failures on nightlies - - os: ubuntu-latest - arch: x64 - version: nightly - experimental: true - - os: ubuntu-latest - arch: x86 - version: nightly - experimental: true - # test macOS and Windows with latest Julia only - - os: macOS-latest - arch: x64 - version: 1 - experimental: true - - os: windows-latest - arch: x64 - version: 1 - experimental: false - - os: windows-latest - arch: x86 - version: 1 - experimental: false steps: - - uses: actions/checkout@v2 - - name: setup protoc - uses: arduino/setup-protoc@v1 - with: - version: '3.x' - - run: protoc --version - - uses: julia-actions/setup-julia@v1 - with: - version: ${{ matrix.version }} - arch: ${{ matrix.arch }} - - uses: actions/cache@v1 - env: - cache-name: cache-artifacts - with: - path: ~/.julia/artifacts - key: ${{ runner.os }}-test-${{ env.cache-name }}-${{ hashFiles('**/Project.toml') }} - restore-keys: | - ${{ runner.os }}-test-${{ env.cache-name }}- - ${{ runner.os }}-test- - ${{ runner.os }}- - - uses: julia-actions/julia-buildpkg@v1 - - uses: julia-actions/julia-runtest@v1 - - uses: julia-actions/julia-processcoverage@v1 - - uses: codecov/codecov-action@v1 - with: - file: lcov.info \ No newline at end of file + - uses: actions/checkout@v5 + - name: Install uv + uses: astral-sh/setup-uv@v6 + - name: Install Python + run: uv python install + working-directory: test/python + - name: Run Python gRPC Test Server + run: uv run grpc_test_server.py test & + working-directory: test/python + - uses: julia-actions/setup-julia@v1 + with: + version: ${{ matrix.version }} + arch: ${{ matrix.arch }} + - uses: actions/cache@v4 + env: + cache-name: cache-artifacts + with: + path: ~/.julia/artifacts + key: ${{ runner.os }}-test-${{ env.cache-name }}-${{ hashFiles('**/Project.toml') }} + restore-keys: | + ${{ runner.os }}-test-${{ env.cache-name }}- + ${{ runner.os }}-test- + ${{ runner.os }}- + - uses: julia-actions/julia-buildpkg@v1 + - uses: julia-actions/julia-runtest@v1 + - name: Stop Python gRPC Test Serer + run: killall uv + - uses: julia-actions/julia-processcoverage@v1 + - uses: codecov/codecov-action@v5 + with: + token: ${{ secrets.CODECOV_TOKEN }} diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000..9760ec1 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,37 @@ +name: Documentation + +on: + push: + branches: + - master # update to match your development branch (master, main, dev, trunk, ...) + tags: '*' + pull_request: + +jobs: + build: + # These permissions are needed to: + # - Deploy the documentation: https://documenter.juliadocs.org/stable/man/hosting/#Permissions + # - Delete old caches: https://github.com/julia-actions/cache#usage + permissions: + actions: write + contents: write + pull-requests: read + statuses: write + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: julia-actions/setup-julia@v2 + with: + version: '1' + - uses: julia-actions/cache@v2 + - name: Install dependencies + shell: julia --color=yes --project=docs {0} + run: | + using Pkg + Pkg.develop(PackageSpec(path=pwd())) + Pkg.instantiate() + - name: Build and deploy + run: julia --color=yes --project=docs docs/make.jl + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # If authenticating with GitHub Actions token + DOCUMENTER_KEY: ${{ secrets.DOCUMENTER_KEY }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0b50be2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +.venv +.idea +__pycache__ +*.pyc +build +Manifest.toml \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..0924bdb --- /dev/null +++ b/LICENSE @@ -0,0 +1,23 @@ +MIT License + +Copyright (c) 2025: Carroll Vance +Copyright (c) 2021: Julia Computing, Inc. +Copyright (c) 2020 Stefan Karpinski and contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/LICENSE.md b/LICENSE.md deleted file mode 100644 index ce44893..0000000 --- a/LICENSE.md +++ /dev/null @@ -1,22 +0,0 @@ -The gRPCClient.jl package is licensed under the MIT "Expat" License: - -> Copyright (c) 2021: Julia Computing, Inc. -> -> Permission is hereby granted, free of charge, to any person obtaining a copy -> of this software and associated documentation files (the "Software"), to deal -> in the Software without restriction, including without limitation the rights -> to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -> copies of the Software, and to permit persons to whom the Software is -> furnished to do so, subject to the following conditions: -> -> The above copyright notice and this permission notice shall be included in all -> copies or substantial portions of the Software. -> -> THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -> IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -> FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -> AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -> LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -> OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -> SOFTWARE. -> diff --git a/Project.toml b/Project.toml index 3e88663..5345002 100644 --- a/Project.toml +++ b/Project.toml @@ -1,23 +1,23 @@ name = "gRPCClient" uuid = "aaca4a50-36af-4a1d-b878-4c443f2061ad" -authors = ["Tanmay K.M. "] -version = "0.1.4" +authors = ["Carroll Vance "] +version = "1.0.0" [deps] -Downloads = "f43a241f-c20a-4ad4-852c-f6b1247861c6" +FileWatching = "7b1f6079-737a-58dc-b8bc-7a2ca5c1b5ee" LibCURL = "b27032c2-a3e7-50c8-80cd-2d36dbcbfd21" +PrecompileTools = "aea7be01-6a6a-4083-8856-8a6e6704d82a" ProtoBuf = "3349acd9-ac6a-5e09-bcdb-63829b23a429" [compat] -Downloads = "1.4" -LibCURL = "0.6" -ProtoBuf = "0.11" -julia = "1.6" +julia = "1.10" +FileWatching = "^1.10" +LibCURL = "~0.6.4" +PrecompileTools = "^1.2" +ProtoBuf = "~1.2" [extras] -Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" -Sockets = "6462fe0b-24de-5631-8697-dd941f90decc" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" [targets] -test = ["Random", "Sockets", "Test"] +test = ["Test"] \ No newline at end of file diff --git a/README.md b/README.md index 079c79c..a69e103 100644 --- a/README.md +++ b/README.md @@ -1,243 +1,94 @@ # gRPCClient.jl -A Julia gRPC Client. +[![License][license-img]][license-url] +[![Documentation][doc-stable-img]][doc-stable-url] +[![Documentation][doc-dev-img]][doc-dev-url] +[![CI](https://github.com/csvance/gRPCClient2.jl/actions/workflows/ci.yml/badge.svg)](https://github.com/csvance/gRPCClient2.jl/actions/workflows/ci.yml) +[![codecov](https://codecov.io/github/csvance/gRPCClient2.jl/graph/badge.svg?token=2SUFGIE336)](https://codecov.io/github/csvance/gRPCClient2.jl) -[![Build Status](https://github.com/JuliaComputing/gRPCClient.jl/workflows/CI/badge.svg)](https://github.com/JuliaComputing/gRPCClient.jl/actions?query=workflow%3ACI+branch%3Amain) -[![codecov.io](http://codecov.io/github/JuliaComputing/gRPCClient.jl/coverage.svg?branch=main)](http://codecov.io/github/JuliaComputing/gRPCClient.jl?branch=main) +gRPCClient.jl aims to be a production grade gRPC client emphasizing performance and reliability. -## Generating gRPC Service Client +## Documentation -gRPC services are declared in `.proto` files. Use `gRPCClient.generate` to generate client code from specification files. +The documentation for gRPCClient.jl can be found [here](https://csvance.github.io/gRPCClient2.jl). -gRPC code generation uses `protoc` and the `ProtoBuf.jl` package. To be able to generate gRPC client code, `ProtoBuf` package must be installed along with `gRPCClient`. +## Benchmarks -The `protoc` file must have service generation turned on for at least one of C++, python or Java, e.g. one of: - -``` -option cc_generic_services = true; -option py_generic_services = true; -option java_generic_services = true; -``` - -The Julia code generated can be improper if the `package` name declared in the proto specification has `.`. Set a suitable `package` name without `.`. +To run the benchmarks, start a Julia terminal and include the `workloads.jl` file: ```julia -julia> using Pkg - -julia> Pkg.add("ProtoBuf") -... - Installed ProtoBuf ──── v0.11.0 -Downloading artifact: protoc -... -julia> Pkg.add("gRPCClient") -... -julia> # or Pkg.develop(PackageSpec(url="https://github.com/JuliaComputing/gRPCClient.jl")) - -julia> using gRPCClient - -julia> gRPCClient.generate("route_guide.proto") -┌ Info: Generating gRPC client -│ proto = "RouteguideClients/route_guide.proto" -└ outdir = "RouteguideClients" -┌ Info: Detected -│ package = "routeguide" -└ service = "RouteGuide" -┌ Info: Generated -└ outdir = "RouteguideClients" -``` - -The generated code can either be published as a package or included and used as a module. - -```julia -julia> using gRPCClient - -julia> include("RouteguideClients/RouteguideClients.jl"); - -julia> using .RouteguideClients - -julia> import .RouteguideClients: Point, Feature, GetFeature - -julia> Base.show(io::IO, location::Point) = - print(io, string("[", location.latitude, ", ", location.longitude, "]")) - -julia> Base.show(io::IO, feature::Feature) = - print(io, string(feature.name, " - ", feature.location)) - -julia> client = RouteGuideBlockingClient("https://server:10000/"); - -julia> point = Point(; latitude=409146138, longitude=-746188906); # request param - -julia> feature, status_future = GetFeature(client, point); - -julia> gRPCCheck(status_future) # check status of request -true - -julia> feature # this is the API return value -Berkshire Valley Management Area Trail, Jefferson, NJ, USA - [409146138, -746188906] +include("workloads.jl") ``` -The generated module is named after the package declared in the proto file. -And for each service, a pair of clients are generated in the form of -`Client` and `BlockingClient`. - -The service methods generated for `Client` are identical to the -ones generated for `BlockingClient`, except that they spawn off -the actual call into a task and accept a callback method that is invoked with -the results. The `BlockingClient` may however be more intuitive -to use. +All of the benchmarks use the asynchronous channels interface to run multiple requests at the same time. All benchmark tests run against the Test gRPC Server in `test/python`. See the relevant [documentation](https://csvance.github.io/gRPCClient2.jl/dev/#Test-gRPC-Server) for information on how to run this. -Each service method returns (or calls back with, in the case of non-blocking -clients) two values: -- The result, which can be a Julia struct or a `Channel` for streaming output. -- And, the gRPC status. +### "smol" -The `gRPCCheck` method checks the status for success or failure. Note that for -methods with streams as input or output, the gRPC status will not be ready -until the method completes. So the status check and stream use must be done -in separate tasks. E.g.: +Smol benchmarks sending and recieving lots of extremely small protobuffs (~16 bytes each) ```julia -@sync begin - in_channel = Channel{RouteguideClients.RouteNote}(1) - @async begin - # send inputs - for input in inputs - put!(in_channel, input) - end - close(in_channel) - end - out_channel, status_future = RouteguideClients.RouteChat(client, in_channel) - @async begin - # consume outputs - for output in out_channel - # use output - end - end - @async begin - gRPCCheck(status_future) - end -end -``` +# Note that there are 1_000 RPC calls per sample, so the mean should be divided by 1_000 +julia> benchmark_workload_smol() +BenchmarkTools.Trial: 41 samples with 1 evaluation per sample. + Range (min … max): 108.345 ms … 135.084 ms ┊ GC (min … max): 0.00% … 7.75% + Time (median): 123.482 ms ┊ GC (median): 0.00% + Time (mean ± σ): 122.444 ms ± 6.091 ms ┊ GC (mean ± σ): 0.33% ± 1.41% -## APIs and Implementation Details + █ ▃▃▃█ █ █ ▃ + ▇▁▇▁▁▁▁▇▇▁▁▁▇▁▁▁▁▇▇▇▇▇▁▇▁▇▇▁▁█▁▇▁████▁█▇▁█▇█▁▁▁▁▁▇▇▇▁▁▁▁▇▁▁▁▇ ▁ + 108 ms Histogram: frequency by time 135 ms < -The generated gRPC client (`RouteGuideBlockingClient` in the example above) -uses a gRPC controller and channel behind the scenes to communicate with -the server. + Memory estimate: 4.27 MiB, allocs estimate: 93559. + ``` -### `gRPCController` + The mean RPC throughput is 8166 request/sec. -A `gRPCController` contains settings to control the behavior of gRPC requests. -Each gRPC client holds an instance of the controller created using keyword -arguments passed to its constructor. + ### (32, 224, 224) UInt8 Batch Inference -```julia -gRPCController(; - [ maxage::Int = 0, ] - [ keepalive::Int64 = 60, ] - [ negotiation::Symbol = :http2_prior_knowledge, ] - [ revocation::Bool = true, ] - [ request_timeout::Real = Inf, ] - [ connect_timeout::Real = 0, ] - [ max_message_length = DEFAULT_MAX_MESSAGE_LENGTH, ] - [ max_recv_message_length = 0, ] - [ max_send_message_length = 0, ] - [ enable_shared_locks = false, ] - [ verbose::Bool = false, ] -) -``` - -- `maxage`: maximum age (seconds) of a connection beyond which it will not - be reused (default 180 seconds, same as setting this to 0). -- `keepalive`: interval (seconds) in which to send TCP keepalive messages on - the connection (default 60 seconds). -- `negotiation`: how to negotiate HTTP2, can be one of `:http2_prior_knowledge` - (no negotiation, the default), `:http2_tls` (http2 upgrade but only over - tls), or `:http2` (http2 upgrade) -- `revocation`: whether to check for certificate recovation (default is true) -- `request_timeout`: request timeout (seconds) -- `connect_timeout`: connect timeout (seconds) (default is 300 seconds, same - as setting this to 0) -- `max_message_length`: maximum message length (default is 4MB) -- `max_recv_message_length`: maximum message length to receive (default is - `max_message_length`, same as setting this to 0) -- `max_send_message_length`: maximum message length to send (default is - `max_message_length`, same as setting this to 0) -- `enable_shared_locks`: whether to enable locks for using gRPCClient across - tasks/threads concurrently (experimental, default is false) -- `verbose`: whether to print out verbose communication logs (default false) - -### `gRPCChannel` + This benchmark simulates sending 224x224 UInt8 images in a batch size of 32 for inference (~1.6 MB each) ```julia -gRPCChannel(baseurl::String) +# Note that there are 100 RPC calls per sample, so the mean should be divided by 100 +julia> benchmark_workload_32_224_224_uint8() +BenchmarkTools.Trial: 27 samples with 1 evaluation per sample. + Range (min … max): 151.472 ms … 225.548 ms ┊ GC (min … max): 1.17% … 10.24% + Time (median): 186.578 ms ┊ GC (median): 1.32% + Time (mean ± σ): 187.099 ms ± 17.970 ms ┊ GC (mean ± σ): 3.72% ± 4.59% + + ▃ ▃ █ ▃ + ▇▁▁▁▁▁▇▁▁▁▁▇█▁▇▇▁▁▁▁▁▁▇█▁▁▇▁█▁█▇▁▁▇▁▁▇▇▇▇▁▁▇▇▁▁▇▁▁▁▁▁▁▁▁▇▁▁▁▇ ▁ + 151 ms Histogram: frequency by time 226 ms < + + Memory estimate: 64.00 MiB, allocs estimate: 12943. ``` -`gRPCChannel` represents a connection to a specific service endpoint -(service `baseurl`) of a gRPC server. - -A channel also usually has a single network connection backing it and -multiple streams of requests can flow through it at any time. The number -of streams that can be multiplexed is negotiated between the client and -the server. - -### `gRPCStatus` - -`gRPCStatus` represents the status of a request. It has the following fields: +The mean RPC throughput is ~534 request/sec. -- `success`: whether the request was completed successfully. -- `grpc_status`: the grpc status code returned -- `message`: any error message if request was not successful +## Stress Testing -### `gRPCCheck` +To run the stress tests, start a Julia terminal and include the `workloads.jl` file: ```julia -gRPCCheck(status; throw_error::Bool=true) +include("workloads.jl") ``` -Method to check the response of a gRPC request and raise a `gRPCException` -if it has failed. If `throw_error` is set to false, returns `true` or `false` -indicating success instead. - -### `gRPCException` - -Every gRPC request returns the result and a future representing the status -of the gRPC request. Use the `gRPCCheck` method on the status future to check -the request status and throw a `gRPCException` if it is not successful. - -The abstract `gRPCException` type has the following concrete implementations: - -- `gRPCMessageTooLargeException` -- `gRPCServiceCallException` - -### `gRPCMessageTooLargeException` - -A `gRPMessageTooLargeException` exception is thrown when a message is -encountered that has a size greater than the limit configured. -Specifically, `max_recv_message_length` while receiving and -`max_send_message_length` while sending. - -A `gRPMessageTooLargeException` has the following members: - -- `limit`: the limit value that was exceeded -- `encountered`: the amount of data that was actually received - or sent before this error was triggered. Note that this may - not correspond to the full size of the data, as error may be - thrown before actually materializing the complete data. +Stress tests are available corresponding to each benchmark listed above: -### `gRPCServiceCallException` +- `stress_workload_smol()` +- `stress_workload_32_224_224_uint8()` -A `gRPCServiceCallException` is thrown if a gRPC request is not successful. -It has the following members: +These run forever, and are useful to help identify any stability issues or resource leaks. -- `grpc_status`: grpc status code for this request -- `message`: any error message if request was not successful +## Acknowledgement -## TODO +This package is essentially a rewrite of the 0.1 version of gRPCClient.jl together with a gRPC specialized version of [Downloads.jl](https://github.com/JuliaLang/Downloads.jl). Without the above packages to build ontop of this effort would have been a far more signifigant undertaking, so thank you to all of the authors and maintainers who made this possible. -- Concurrent use of gRPCClient is still somewhat flaky. +[license-url]: ./LICENSE +[license-img]: http://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat -## Credits +[doc-dev-img]: https://img.shields.io/badge/docs-dev-blue.svg +[doc-dev-url]: https://csvance.github.io/gRPCClient2.jl/dev/ -This package was originally developed at [Julia Computing](https://juliacomputing.com) +[doc-stable-img]: https://img.shields.io/badge/docs-stable-blue.svg +[doc-stable-url]: https://csvance.github.io/gRPCClient2.jl/stable/ \ No newline at end of file diff --git a/docs/.gitignore b/docs/.gitignore new file mode 100644 index 0000000..a303fff --- /dev/null +++ b/docs/.gitignore @@ -0,0 +1,2 @@ +build/ +site/ diff --git a/docs/Project.toml b/docs/Project.toml new file mode 100644 index 0000000..dfa65cd --- /dev/null +++ b/docs/Project.toml @@ -0,0 +1,2 @@ +[deps] +Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" diff --git a/docs/make.jl b/docs/make.jl new file mode 100644 index 0000000..b97a743 --- /dev/null +++ b/docs/make.jl @@ -0,0 +1,15 @@ +using Documenter +using gRPCClient + +makedocs( + sitename = "gRPCClient.jl", + format = Documenter.HTML(), + modules = [gRPCClient] +) + +# Documenter can also automatically deploy documentation to gh-pages. +# See "Hosting Documentation" and deploydocs() in the Documenter manual +# for more information. +deploydocs( + repo = "github.com/JuliaComputing/gRPCClient.jl.git" +) diff --git a/docs/src/index.md b/docs/src/index.md new file mode 100644 index 0000000..7eab10e --- /dev/null +++ b/docs/src/index.md @@ -0,0 +1,91 @@ +# gRPCClient.jl + +gRPCClient.jl aims to be a production grade gRPC client emphasizing performance and reliability. + +## Features + +- Unary+Streaming RPC +- HTTP/2 connection multiplexing +- Synchronous and asynchronous interfaces +- Thread safe +- SSL/TLS + +The client is missing a few features which will be added over time if there is sufficient interest: + +- OAuth2 +- Compression + +## Getting Started + +### Test gRPC Server + +All examples in the documentation are run against a test server written in Python. You can run it by doing the following: + +```bash +# Install uv package manager - see https://docs.astral.sh/uv/#installation for more details +curl -LsSf https://astral.sh/uv/install.sh | sh + +# Change directory to the python test server project +cd test/python + +# Run the test server +uv run grpc_test_server.py + +``` + +### Code Generation + +**Note: support for this is currently being upstreamed into [ProtoBuf.jl](https://github.com/JuliaIO/ProtoBuf.jl/pull/283). Until then, make sure you add the feature branch with gRPC code generation support:** + +`pkg> add https://github.com/csvance/ProtoBuf.jl#external-service-support` + +gRPCClient.jl integrates with ProtoBuf.jl to automatically generate Julia client stubs for calling gRPC. + +```julia +using ProtoBuf +using gRPCClient + +# Register our service codegen with ProtoBuf.jl +grpc_register_service_codegen() + +# Creates Julia bindings for the messages and RPC defined in test.proto +protojl("test/proto/test.proto", ".", "test/gen") +``` + +## Example Usage + +See [here](#RPC) for examples covering all provided interfaces for both unary and streaming gRPC calls. + +## Package Initialization / Shutdown + +```@docs +grpc_init() +grpc_shutdown() +grpc_global_handle() +``` + +## RPC + +### Unary + +```@docs +grpc_async_request(client::gRPCServiceClient{TRequest,false,TResponse,false}, request::TRequest) where {TRequest<:Any,TResponse<:Any} +grpc_async_request(client::gRPCServiceClient{TRequest,false,TResponse,false}, request::TRequest, channel::Channel{gRPCAsyncChannelResponse{TResponse}}, index::Int64) where {TRequest<:Any,TResponse<:Any} +grpc_async_await(client::gRPCServiceClient{TRequest,false,TResponse,false}, request::gRPCRequest) where {TRequest<:Any,TResponse<:Any} +grpc_sync_request(client::gRPCServiceClient{TRequest,false,TResponse,false}, request::TRequest) where {TRequest<:Any,TResponse<:Any} +``` + +### Streaming + +```@docs +grpc_async_request(client::gRPCServiceClient{TRequest,true,TResponse,false}, request::Channel{TRequest}) where {TRequest<:Any,TResponse<:Any} +grpc_async_request(client::gRPCServiceClient{TRequest,false,TResponse,true},request::TRequest,response::Channel{TResponse}) where {TRequest<:Any,TResponse<:Any} +grpc_async_request(client::gRPCServiceClient{TRequest,true,TResponse,true},request::Channel{TRequest},response::Channel{TResponse}) where {TRequest<:Any,TResponse<:Any} +grpc_async_await(client::gRPCServiceClient{TRequest,true,TResponse,false},request::gRPCRequest) where {TRequest<:Any,TResponse<:Any} +``` + +## Exceptions + +```@docs +gRPCServiceCallException +``` \ No newline at end of file diff --git a/src/Curl.jl b/src/Curl.jl new file mode 100644 index 0000000..3324edb --- /dev/null +++ b/src/Curl.jl @@ -0,0 +1,735 @@ +const CURL_VERSION_STR = unsafe_string(curl_version()) +let m = match(r"^libcurl/(\d+\.\d+\.\d+)\b", CURL_VERSION_STR) + m !== nothing || error("unexpected CURL_VERSION_STR value") + curl = m.captures[1] + julia = "$(VERSION.major).$(VERSION.minor)" + const global CURL_VERSION = VersionNumber(curl) + const global USER_AGENT = "curl/$curl julia/$julia" +end + + +function write_callback( + data::Ptr{Cchar}, + size::Csize_t, + count::Csize_t, + req_p::Ptr{Cvoid}, +)::Csize_t + try + req = unsafe_pointer_to_objref(req_p)::gRPCRequest + + n = size * count + buf = unsafe_wrap(Array, convert(Ptr{UInt8}, data), (n,)) + + handled_n_bytes_total = 0 + try + while !isnothing(buf) && handled_n_bytes_total < n + handled_n_bytes, buf = handle_write(req, buf) + handled_n_bytes_total += handled_n_bytes + handled_n_bytes == 0 && break + end + catch ex + # Eat InvalidStateException raised on put! to closed channel + !isa(ex, InvalidStateException) && rethrow(ex) + end + + # If there was an exception handle it + !isnothing(req.ex) && return typemax(Csize_t) + + # Check that we handled the correct number of bytes + # If there was no exception in handle_write this should always match + if handled_n_bytes_total != n + req.ex = gRPCServiceCallException( + GRPC_INTERNAL, + "Recieved $(n) bytes from curl but only handled $(handled_n_bytes_total)", + ) + return typemax(Csize_t) + end + + return handled_n_bytes_total + catch err + @error("write_callback: unexpected error", err = err, maxlog = 1_000) + return typemax(Csize_t) + end +end + +function read_callback( + data::Ptr{Cchar}, + size::Csize_t, + count::Csize_t, + req_p::Ptr{Cvoid}, +)::Csize_t + try + req = unsafe_pointer_to_objref(req_p)::gRPCRequest + + # Check for race condition + @assert !req.curl_done_reading.set + + buf_p = pointer(req.request.data) + req.request_ptr + n_left = req.request.size - req.request_ptr + + n = size * count + n_min = min(n, n_left) + + ccall(:memcpy, Ptr{Cvoid}, (Ptr{Cvoid}, Ptr{Cvoid}, Csize_t), data, buf_p, n_min) + + req.request_ptr += n_min + + if isstreaming_request(req) && n_min == 0 + # Keep sending until the channel is closed and empty + if !isopen(req.request_c) && isempty(req.request_c) + notify(req.curl_done_reading) + return 0 + end + + seekstart(req.request) + truncate(req.request, 0) + req.request_ptr = 0 + + # Safe to write more data to the request buffer again + notify(req.curl_done_reading) + + return CURL_READFUNC_PAUSE + end + + return n_min + catch err + @error("read_callback: unexpected error", err = err, maxlog = 1_000) + return CURL_READFUNC_ABORT + end +end + +const regex_grpc_status = r"grpc-status: ([0-9]+)" +const regex_grpc_message = Regex("grpc-message: (.*)", "s") + + +function header_callback( + data::Ptr{Cchar}, + size::Csize_t, + count::Csize_t, + req_p::Ptr{Cvoid}, +)::Csize_t + try + req = unsafe_pointer_to_objref(req_p)::gRPCRequest + n = size * count + + header = unsafe_string(data, n) + header = strip(header) + + if (m_grpc_status = match(regex_grpc_status, header)) !== nothing + req.grpc_status = parse(UInt64, m_grpc_status.captures[1]) + elseif (m_grpc_message = match(regex_grpc_message, header)) !== nothing + req.grpc_message = m_grpc_message.captures[1] + end + + return n + catch err + @error("header_callback: unexpected error", err = err, maxlog = 1_000) + return typemax(Csize_t) + end +end + + + +mutable struct gRPCRequest + # CURL multi lock for exclusive access to the easy handle after its added to the multi + lock::ReentrantLock + + # CURL easy handle + easy::Ptr{Cvoid} + # CURL multi handle + multi::Ptr{Cvoid} + + # The full request URL + url::String + + # Contains the request data which will be uploaded in read_callback + request::IOBuffer + + # Tracks the current location inside request for the read_callback + request_ptr::Int64 + + # Holds the current response in the response stream + response::IOBuffer + + # These are only used when the request or response is streaming + request_c::Union{Nothing,Channel{IOBuffer}} + response_c::Union{Nothing,Channel{IOBuffer}} + + # The task making the request can block on this until the request is complete + ready::Event + + # CURL status code and error message + code::CURLcode + errbuf::Vector{UInt8} + + # Used to enforce maximum send / recv message sizes + max_send_message_length::Int64 + max_recieve_message_length::Int64 + + # Contains the first exception if any encountered during the request + ex::Union{Nothing,Exception} + + # Keeps track of the response stream parsing state + response_read_header::Bool + response_compressed::Bool + response_length::UInt32 + + # When this is set we can write to the request upload buffer because curl is not reading from it + curl_done_reading::Event + + # Response headers + grpc_status::Int64 + grpc_message::String + + function gRPCRequest( + grpc, + url, + request::IOBuffer, + response::IOBuffer, + request_c::Union{Nothing,Channel{IOBuffer}}, + response_c::Union{Nothing,Channel{IOBuffer}}; + deadline = 10, + keepalive = 60, + max_send_message_length = 4 * 1024 * 1024, + max_recieve_message_length = 4 * 1024 * 1024, + ) + # Reduce number of available requests by one or block if its currently zero + acquire(grpc.sem) + + easy_handle = curl_easy_init() + + # Uncomment this for debugging purposes + # curl_easy_setopt(easy_handle, CURLOPT_VERBOSE, UInt32(1)) + + http_url = replace(url, "grpc://" => "http://") + http_url = replace(http_url, "grpcs://" => "https://") + + curl_easy_setopt(easy_handle, CURLOPT_URL, http_url) + curl_easy_setopt(easy_handle, CURLOPT_TIMEOUT, deadline) + curl_easy_setopt(easy_handle, CURLOPT_PIPEWAIT, Clong(1)) + curl_easy_setopt(easy_handle, CURLOPT_POST, Clong(1)) + curl_easy_setopt(easy_handle, CURLOPT_CUSTOMREQUEST, "POST") + + if startswith(http_url, "http://") + curl_easy_setopt( + easy_handle, + CURLOPT_HTTP_VERSION, + CURL_HTTP_VERSION_2_PRIOR_KNOWLEDGE, + ) + elseif startswith(http_url, "https://") + curl_easy_setopt(easy_handle, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_2TLS) + end + + function grpc_timeout_header_val(timeout::Real) + if round(Int, timeout) == timeout + timeout_secs = round(Int64, timeout) + return "$(timeout_secs)S" + end + timeout *= 1000 + if round(Int, timeout) == timeout + timeout_millisecs = round(Int64, timeout) + return "$(timeout_millisecs)m" + end + timeout *= 1000 + if round(Int, timeout) == timeout + timeout_microsecs = round(Int64, timeout) + return "$(timeout_microsecs)u" + end + timeout *= 1000 + timeout_nanosecs = round(Int64, timeout) + return "$(timeout_nanosecs)n" + end + + headers = C_NULL + headers = curl_slist_append(headers, "User-Agent: $(USER_AGENT)") + headers = curl_slist_append(headers, "Content-Type: application/grpc+proto") + headers = curl_slist_append(headers, "Content-Length:") + headers = curl_slist_append(headers, "te: trailers") + headers = + curl_slist_append(headers, "grpc-timeout: $(grpc_timeout_header_val(deadline))") + curl_easy_setopt(easy_handle, CURLOPT_HTTPHEADER, headers) + + curl_easy_setopt(easy_handle, CURLOPT_TCP_KEEPALIVE, Clong(1)) + curl_easy_setopt(easy_handle, CURLOPT_TCP_KEEPINTVL, keepalive) + curl_easy_setopt(easy_handle, CURLOPT_TCP_KEEPIDLE, keepalive) + + req = new( + grpc.lock, + easy_handle, + grpc.multi, + http_url, + request, + 0, + response, + request_c, + response_c, + Event(), + UInt32(0), + zeros(UInt8, CURL_ERROR_SIZE), + max_send_message_length, + max_recieve_message_length, + nothing, + false, + false, + 0, + Event(), + GRPC_OK, + "", + ) + + req_p = pointer_from_objref(req) + curl_easy_setopt(easy_handle, CURLOPT_PRIVATE, req_p) + + errbuf_p = pointer(req.errbuf) + curl_easy_setopt(easy_handle, CURLOPT_ERRORBUFFER, errbuf_p) + + write_cb = + @cfunction(write_callback, Csize_t, (Ptr{Cchar}, Csize_t, Csize_t, Ptr{Cvoid})) + curl_easy_setopt(easy_handle, CURLOPT_WRITEFUNCTION, write_cb) + curl_easy_setopt(easy_handle, CURLOPT_WRITEDATA, req_p) + + read_cb = + @cfunction(read_callback, Csize_t, (Ptr{Cchar}, Csize_t, Csize_t, Ptr{Cvoid})) + curl_easy_setopt(easy_handle, CURLOPT_READFUNCTION, read_cb) + curl_easy_setopt(easy_handle, CURLOPT_READDATA, req_p) + + # set header callback + header_cb = + @cfunction(header_callback, Csize_t, (Ptr{Cchar}, Csize_t, Csize_t, Ptr{Cvoid})) + + + curl_easy_setopt(easy_handle, CURLOPT_HEADERFUNCTION, header_cb) + curl_easy_setopt(easy_handle, CURLOPT_HEADERDATA, req_p) + curl_easy_setopt(easy_handle, CURLOPT_UPLOAD, true) + + + lock(grpc.lock) do + if !grpc.running + curl_easy_cleanup(easy_handle) + throw( + gRPCServiceCallException( + GRPC_FAILED_PRECONDITION, + "Tried to make a request when the provided grpc handle is shutdown", + ), + ) + end + + push!(grpc.requests, req) + curl_multi_add_handle(grpc.multi, easy_handle) + end + + return req + end +end + +isstreaming_request(req::gRPCRequest) = !isnothing(req.request_c) +isstreaming_response(req::gRPCRequest) = !isnothing(req.response_c) + +Base.wait(req::gRPCRequest) = wait(req.ready) +Base.reset(req::gRPCRequest) = reset(req.ready) + + +function handle_write(req::gRPCRequest, buf::Vector{UInt8})::Tuple{Int64, Union{Nothing, Vector{UInt8}}} + if !req.response_read_header + header_bytes_left = GRPC_HEADER_SIZE - req.response.size + + if length(buf) < header_bytes_left + # Not enough data yet to read the entire header + return write(req.response, buf), nothing + else + + buf_header = buf[1:header_bytes_left] + n = write(req.response, buf_header) + + # Read the header + seekstart(req.response) + req.response_compressed = read(req.response, UInt8) > 0 + req.response_length = ntoh(read(req.response, UInt32)) + + if req.response_compressed + req.ex = gRPCServiceCallException( + GRPC_UNIMPLEMENTED, + "Response was compressed but compression is not currently supported.", + ) + notify(req.ready) + return n, nothing + elseif req.response_length > req.max_recieve_message_length + req.ex = gRPCServiceCallException( + GRPC_RESOURCE_EXHAUSTED, + "length-prefix longer than max_recieve_message_length: $(req.response_length) > $(req.max_recieve_message_length)", + ) + notify(req.ready) + return n, nothing + end + + req.response_read_header = true + seekstart(req.response) + truncate(req.response, 0) + + buf_leftover = nothing + + if (leftover_bytes = length(buf) - header_bytes_left) > 0 + # Handle the remaining data + buf_leftover = unsafe_wrap(Array, pointer(buf) + n, (leftover_bytes,)) + end + + return n, buf_leftover + end + end + + # Already read the header + message_bytes_left = req.response_length - req.response.size + + # Not enough bytes to complete the message + length(buf) < message_bytes_left && return write(req.response, buf), nothing + + if isstreaming_response(req) + # Write just enough to complete the message + buf_complete = unsafe_wrap(Array, pointer(buf), (message_bytes_left,)) + n = write(req.response, buf_complete) + + # Response is done, put it in the channel so it can be returned back to the user + seekstart(req.response) + + # Put the completed response protobuf buffer in the channel so it can be processed by the `grpc_async_stream_response` task + put!(req.response_c, req.response) + + # There might be another response after this so reset these + req.response = IOBuffer() + req.response_read_header = false + req.response_compressed = false + req.response_length = 0 + + # Handle the remaining data + leftover_bytes = message_bytes_left - n + + buf_leftover = nothing + if leftover_bytes > 0 + buf_leftover = unsafe_wrap(Array, pointer(buf) + n, (length(leftover_bytes),)) + end + + return n, buf_leftover + else + # We only expect a single response for non-streaming RPC + if length(buf) > message_bytes_left + req.ex = gRPCServiceCallException( + GRPC_RESOURCE_EXHAUSTED, + "Response was longer than declared in length-prefix.", + ) + notify(req.ready) + return 0, nothing + end + + n = write(req.response, buf) + seekstart(req.response) + + return n, nothing + end + +end + + +function timer_callback(multi_h::Ptr{Cvoid}, timeout_ms::Clong, grpc_p::Ptr{Cvoid})::Cint + try + grpc = unsafe_pointer_to_objref(grpc_p)::gRPCCURL + @assert multi_h == grpc.multi + + stoptimer!(grpc) + + if timeout_ms >= 0 + grpc.timer = Timer(timeout_ms / 1000) do timer + lock(grpc.lock) do + !grpc.running && return + curl_multi_socket_action( + grpc.multi, + CURL_SOCKET_TIMEOUT, + 0, + Ref{Cint}(), + ) + check_multi_info(grpc) + end + end + end + + return 0 + catch err + @error("timer_callback: unexpected error", err = err, maxlog = 1_000) + return -1 + end +end + + +mutable struct CURLWatcher + sock::curl_socket_t + fdw::FDWatcher + ready::Event + running::Bool + + + function CURLWatcher(sock, fdw) + event = Event() + notify(event) + new(sock, fdw, event, true) + end +end + +Base.isreadable(w::CURLWatcher) = w.fdw.readable +Base.iswritable(w::CURLWatcher) = w.fdw.writable +function Base.close(w::CURLWatcher) + w.running = false + notify(w.ready) + close(w.fdw) +end + + +function socket_callback( + easy_h::Ptr{Cvoid}, + sock::curl_socket_t, + action::Cint, + grpc_p::Ptr{Cvoid}, + socket_p::Ptr{Cvoid}, +)::Cint + try + if action ∉ (CURL_POLL_IN, CURL_POLL_OUT, CURL_POLL_INOUT, CURL_POLL_REMOVE) + @error("socket_callback: unexpected action", action, maxlog = 1_000) + return -1 + end + + grpc = unsafe_pointer_to_objref(grpc_p)::gRPCCURL + + # Handle is being shutdown, give exclusive access of watchers to close() + !grpc.running && return 0 + + if action in (CURL_POLL_IN, CURL_POLL_OUT, CURL_POLL_INOUT) + readable = action in (CURL_POLL_IN, CURL_POLL_INOUT) + writable = action in (CURL_POLL_OUT, CURL_POLL_INOUT) + + watcher = nothing + if sock in keys(grpc.watchers) + + # We already have a watcher for this sock + watcher = grpc.watchers[sock] + + # Reset the ready event and trigger an EOFError + reset(watcher.ready) + close(watcher.fdw) + + # Update the FDWatcher with the new flags + watcher.fdw = FDWatcher(OS_HANDLE(sock), readable, writable) + + # Start waiting on the socket with the new flags + notify(watcher.ready) + + return 0 + end + + # Don't have a watcher, create one and start a task + watcher = CURLWatcher(sock, FDWatcher(OS_HANDLE(sock), readable, writable)) + grpc.watchers[sock] = watcher + + task = @async begin + while watcher.running && grpc.running + # Watcher configuration might be changed, wait until its safe to wait on the watcher + wait(watcher.ready) + + events = try + wait(watcher.fdw) + catch err + err isa EOFError && continue + err isa Base.IOError || rethrow() + FileWatching.FDEvent() + end + + flags = + CURL_CSELECT_IN * isreadable(events) + + CURL_CSELECT_OUT * iswritable(events) + + CURL_CSELECT_ERR * (events.disconnect || events.timedout) + + n_recursive_spin = 0 + lock(grpc.lock) do + while ( + status = curl_multi_socket_action( + grpc.multi, + sock, + flags, + Ref{Cint}(), + ) + ) == CURLM_RECURSIVE_API_CALL + n_recursive_spin += 1 + end + @assert status == CURLM_OK + check_multi_info(grpc) + end + n_recursive_spin > 0 && + @warn "curl_multi_socket_action CURLM_RECURSIVE_API_CALL spun $n_recursive_spin times." + end + + # If the multi handle was shutdown, return without doing any operations on it + !grpc.running && return + + # When we shut down the watcher do the check_multi_info in this task to avoid creating a new one + lock(grpc.lock) do + check_multi_info(grpc) + end + end + @isdefined(errormonitor) && errormonitor(task) + else + # Shut down and cleanup the watcher for this socket + watcher = grpc.watchers[sock] + close(watcher) + delete!(grpc.watchers, sock) + end + + return 0 + catch err + @error("socket_callback: unexpected error", err = err, maxlog = 1_000) + return -1 + end +end + + +function grpc_multi_init(grpc) + grpc.multi = curl_multi_init() + + grpc_p = pointer_from_objref(grpc) + + timer_cb = @cfunction(timer_callback, Cint, (Ptr{Cvoid}, Clong, Ptr{Cvoid})) + curl_multi_setopt(grpc.multi, CURLMOPT_TIMERFUNCTION, timer_cb) + curl_multi_setopt(grpc.multi, CURLMOPT_TIMERDATA, grpc_p) + + socket_cb = @cfunction( + socket_callback, + Cint, + (Ptr{Cvoid}, curl_socket_t, Cint, Ptr{Cvoid}, Ptr{Cvoid}) + ) + curl_multi_setopt(grpc.multi, CURLMOPT_SOCKETFUNCTION, socket_cb) + curl_multi_setopt(grpc.multi, CURLMOPT_SOCKETDATA, grpc_p) +end + + +mutable struct gRPCCURL + multi::Ptr{Cvoid} + lock::ReentrantLock + timer::Union{Nothing,Timer} + watchers::Dict{curl_socket_t,CURLWatcher} + running::Bool + requests::Vector{gRPCRequest} + sem::Semaphore + + function gRPCCURL(max_streams = GRPC_MAX_STREAMS) + grpc = new( + Ptr{Cvoid}(0), + ReentrantLock(), + nothing, + Dict{curl_socket_t,CURLWatcher}(), + true, + Vector{gRPCRequest}(), + Semaphore(max_streams), + ) + + grpc_multi_init(grpc) + + finalizer((x) -> close(x), grpc) + + return grpc + end +end + +function Base.close(grpc::gRPCCURL) + # Lock free way to get exclusive access to grpc.watchers + grpc.running = false + sleep(0.25) + + lock(grpc.lock) do + # Already closed + if grpc.multi == Ptr{Cvoid}(0) + return + end + + # Cleanup easy handles + while length(grpc.requests) > 0 + request = pop!(grpc.requests) + curl_multi_remove_handle(grpc.multi, request.easy) + curl_easy_cleanup(request.easy) + # Unblock anything waiting on the request + notify(request.ready) + end + + # Cleanup watchers + while length(grpc.watchers) > 0 + _, watcher = pop!(grpc.watchers) + close(watcher) + end + + curl_multi_cleanup(grpc.multi) + grpc.multi = Ptr{Cvoid}(0) + end + + nothing +end + +function Base.open(grpc::gRPCCURL) + lock(grpc.lock) do + if grpc.multi != Ptr{Cvoid}(0) + return + end + + # Guarantee that we start with a clean slate + grpc.watchers = Dict{curl_socket_t,CURLWatcher}() + grpc.requests = Vector{gRPCRequest}() + grpc.timer = nothing + grpc.sem = Semaphore(grpc.sem.sem_size) + + grpc.running = true + grpc_multi_init(grpc) + end + + nothing +end + +struct CURLMsg + msg::CURLMSG + easy::Ptr{Cvoid} + code::CURLcode +end + +function check_multi_info(grpc::gRPCCURL) + while true + p = curl_multi_info_read(grpc.multi, Ref{Cint}()) + p == C_NULL && return + message = unsafe_load(convert(Ptr{CURLMsg}, p)) + if message.msg == CURLMSG_DONE + easy_handle = message.easy + req_p_ref = Ref{Ptr{Cvoid}}() + curl_easy_getinfo(easy_handle, CURLINFO_PRIVATE, req_p_ref) + req = unsafe_pointer_to_objref(req_p_ref[])::gRPCRequest + @assert easy_handle == req.easy + req.code = message.code + + # Doing the cleanup here helps with lock contention + curl_multi_remove_handle(req.multi, req.easy) + curl_easy_cleanup(req.easy) + + grpc.requests = filter(x -> x !== req, grpc.requests) + + # Request is all done, notify anything blocking on it + notify(req.ready) + + # Allow a new request now that this one is complete + release(grpc.sem) + else + @error("curl_multi_info_read: unknown message", message, maxlog = 1_000) + end + end +end + + + +function stoptimer!(grpc::gRPCCURL) + t = grpc.timer + if t !== nothing + grpc.timer = nothing + close(t) + end + nothing +end diff --git a/src/ProtoBuf.jl b/src/ProtoBuf.jl new file mode 100644 index 0000000..7391e40 --- /dev/null +++ b/src/ProtoBuf.jl @@ -0,0 +1,43 @@ +function service_codegen_handler(io, t::ServiceType, ctx::Context) + namespace = join(ctx.proto_file.preamble.namespace, ".") + service_name = t.name + + + for rpc in t.rpcs + rpc_path = "/$namespace.$service_name/$(rpc.name)" + + request_type = rpc.request_type.name + response_type = rpc.response_type.name + + if rpc.request_type.package_namespace !== nothing + request_type = join([rpc.package_namespace, request_type], ".") + end + if rpc.response_type.package_namespace !== nothing + response_type = join([rpc.package_namespace, response_type], ".") + end + + println(io, "$(service_name)_$(rpc.name)_Client(") + println(io, "\thost, port;") + println(io, "\tsecure=false,") + println(io, "\tgrpc=grpc_global_handle(),") + println(io, "\tdeadline=10,") + println(io, "\tkeepalive=60,") + println(io, "\tmax_send_message_length = 4*1024*1024,") + println(io, "\tmax_recieve_message_length = 4*1024*1024,") + println( + io, + ") = gRPCServiceClient{$request_type, $(rpc.request_stream), $response_type, $(rpc.response_stream)}(", + ) + println(io, "\thost, port, \"$rpc_path\";") + println(io, "\tsecure=secure,") + println(io, "\tgrpc=grpc,") + println(io, "\tdeadline=deadline,") + println(io, "\tkeepalive=keepalive,") + println(io, "\tmax_send_message_length=max_send_message_length,") + println(io, "\tmax_recieve_message_length=max_recieve_message_length,") + println(io, ")\n") + end +end + + +grpc_register_service_codegen() = register_service_codegen(service_codegen_handler) diff --git a/src/Streaming.jl b/src/Streaming.jl new file mode 100644 index 0000000..e2caa4b --- /dev/null +++ b/src/Streaming.jl @@ -0,0 +1,318 @@ +@static if VERSION >= v"1.12" + +function grpc_async_stream_request( + req::gRPCRequest, + channel::Channel{TRequest}, +) where {TRequest<:Any} + try + encode_buf = IOBuffer() + reqs_ready = 0 + + while isnothing(req.ex) + try + # Always do a blocking take! once so we don't spin + request = take!(channel) + grpc_encode_request_iobuffer( + request, + encode_buf; + max_send_message_length = req.max_send_message_length, + ) + reqs_ready += 1 + + # Try to get get more requests within reason to reduce request overhead interfacing with libcurl + while !isempty(channel) && reqs_ready < 100 && encode_buf.size < 65535 + request = take!(channel) + grpc_encode_request_iobuffer( + request, + encode_buf; + max_send_message_length = req.max_send_message_length, + ) + reqs_ready += 1 + end + catch ex + rethrow(ex) + finally + if encode_buf.size > 0 + seekstart(encode_buf) + + # Wait for libCURL to not be reading anymore + wait(req.curl_done_reading) + + # Write all of the encoded protobufs to the request read buffer + write(req.request, encode_buf) + + # Block on the next wait until cleared by the curl read_callback + reset(req.curl_done_reading) + + # Tell curl we have more to send + lock(req.lock) do + curl_easy_pause(req.easy, CURLPAUSE_CONT) + end + + # Reset the encode buffer + reqs_ready = 0 + seekstart(encode_buf) + truncate(encode_buf, 0) + end + end + end + catch ex + close(channel) + close(req.request_c) + + if isa(ex, InvalidStateException) + # Wait for any request data to be flushed by curl + wait(req.curl_done_reading) + + # Trigger a "return 0" in read_callback so curl ends the current request + reset(req.curl_done_reading) + lock(req.lock) do + curl_easy_pause(req.easy, CURLPAUSE_CONT) + end + + elseif isa(ex, gRPCServiceCallException) + if isnothing(req.ex) + req.ex = ex + notify(req.ready) + end + else + if isnothing(req.ex) + req.ex = ex + notify(req.ready) + end + @error "grpc_async_stream_request: unexpected exception" exception = ex + end + end + + nothing +end + +function grpc_async_stream_response( + req::gRPCRequest, + channel::Channel{TResponse}, +) where {TResponse<:Any} + try + while isnothing(req.ex) + response_buf = take!(req.response_c) + response = decode(ProtoDecoder(response_buf), TResponse) + put!(channel, response) + end + catch ex + close(channel) + close(req.response_c) + + if isa(ex, InvalidStateException) + + elseif isa(ex, gRPCServiceCallException) + if isnothing(req.ex) + req.ex = ex + notify(req.ready) + end + else + if isnothing(req.ex) + req.ex = ex + notify(req.ready) + end + @error "grpc_async_stream_response: unexpected exception" exception = ex + end + + end + + nothing +end + +else + +function grpc_async_stream_request( + req::gRPCRequest, + channel::Channel{TRequest}, +) where {TRequest<:Any} + ex = AssertionError("Streaming not supported with Julia < 1.12") + @error exception=ex + req.ex = ex + close(channel) + notify(req.ready) +end + +function grpc_async_stream_response( + req::gRPCRequest, + channel::Channel{TResponse}, +) where {TResponse<:Any} + ex = AssertionError("Streaming not supported with Julia < 1.12") + @error exception=ex + req.ex = ex + close(channel) + notify(req.ready) +end + +end + +""" + grpc_async_request(client::gRPCServiceClient{TRequest,true,TResponse,false}, request::Channel{TRequest}) where {TRequest<:Any,TResponse<:Any} + +Start a requesting streaming gRPC request. + +```julia +using gRPCClient + +grpc_init() +include("test/gen/test/test_pb.jl") + +client = TestService_TestClientStreamRPC_Client("localhost", 8001) +request_c = Channel{TestRequest}(16) +put!(request_c, TestRequest(1, zeros(UInt64, 1))) + +req = grpc_async_request(client, request_c) + +# Must close the request channel when done sending requests +close(request_c) + +# Get the response +test_response = grpc_async_await(client, req) +``` +""" +function grpc_async_request( + client::gRPCServiceClient{TRequest,true,TResponse,false}, + request::Channel{TRequest}, +) where {TRequest<:Any,TResponse<:Any} + + req = gRPCRequest( + client.grpc, + url(client), + IOBuffer(), + IOBuffer(), + Channel{IOBuffer}(16), + nothing; + deadline = client.deadline, + keepalive = client.keepalive, + max_send_message_length = client.max_send_message_length, + max_recieve_message_length = client.max_recieve_message_length, + ) + + request_task = Threads.@spawn grpc_async_stream_request(req, request) + errormonitor(request_task) + + req +end + +""" + grpc_async_request(client::gRPCServiceClient{TRequest,false,TResponse,true},request::TRequest,response::Channel{TResponse}) where {TRequest<:Any,TResponse<:Any} + +Start a response streaming gRPC request. + +```julia +using gRPCClient + +grpc_init() +include("test/gen/test/test_pb.jl") + +client = TestService_TestServerStreamRPC_Client("localhost", 8001) + +response_c = Channel{TestResponse}(16) + +req = grpc_async_request( + client, + TestRequest(1, zeros(UInt64, 1)), + response_c, +) +test_response = take!(response_c) + +# Raise any exceptions encountered during the request +grpc_async_await(req) +``` +""" +function grpc_async_request( + client::gRPCServiceClient{TRequest,false,TResponse,true}, + request::TRequest, + response::Channel{TResponse}, +) where {TRequest<:Any,TResponse<:Any} + + request_buf = grpc_encode_request_iobuffer( + request; + max_send_message_length = client.max_send_message_length, + ) + seekstart(request_buf) + + req = gRPCRequest( + client.grpc, + url(client), + request_buf, + IOBuffer(), + nothing, + Channel{IOBuffer}(16); + deadline = client.deadline, + keepalive = client.keepalive, + max_send_message_length = client.max_send_message_length, + max_recieve_message_length = client.max_recieve_message_length, + ) + + response_task = Threads.@spawn grpc_async_stream_response(req, response) + errormonitor(response_task) + + req +end + +""" + grpc_async_request(client::gRPCServiceClient{TRequest,true,TResponse,true},request::Channel{TRequest},response::Channel{TResponse}) where {TRequest<:Any,TResponse<:Any} + +Start a bidirectional gRPC request. + +```julia +using gRPCClient + +grpc_init() +include("test/gen/test/test_pb.jl") + +client = TestService_TestBidirectionalStreamRPC_Client("localhost", 8001) + +request_c = Channel{TestRequest}(16) +response_c = Channel{TestResponse}(16) + +put!(request_c, TestRequest(1, zeros(UInt64, 1))) +req = grpc_async_request(client, request_c, response_c) +test_response = take!(response_c) + +# Must close the request channel when done sending requests +close(request_c) +# Raise any exceptions encountered during the request +grpc_async_await(req) +``` +""" +function grpc_async_request( + client::gRPCServiceClient{TRequest,true,TResponse,true}, + request::Channel{TRequest}, + response::Channel{TResponse}, +) where {TRequest<:Any,TResponse<:Any} + + req = gRPCRequest( + client.grpc, + url(client), + IOBuffer(), + IOBuffer(), + Channel{IOBuffer}(16), + Channel{IOBuffer}(16); + deadline = client.deadline, + keepalive = client.keepalive, + max_send_message_length = client.max_send_message_length, + max_recieve_message_length = client.max_recieve_message_length, + ) + + request_task = Threads.@spawn grpc_async_stream_request(req, request) + errormonitor(request_task) + + response_task = Threads.@spawn grpc_async_stream_response(req, response) + errormonitor(response_task) + + req +end + + +""" + grpc_async_await(client::gRPCServiceClient{TRequest,true,TResponse,false},request::gRPCRequest) where {TRequest<:Any,TResponse<:Any} + +Raise any exceptions encountered during the streaming request. +""" +grpc_async_await( + client::gRPCServiceClient{TRequest,true,TResponse,false}, + request::gRPCRequest, +) where {TRequest<:Any,TResponse<:Any} = grpc_async_await(request, TResponse) diff --git a/src/Unary.jl b/src/Unary.jl new file mode 100644 index 0000000..8fe5fa8 --- /dev/null +++ b/src/Unary.jl @@ -0,0 +1,168 @@ +# Unary RPC + +""" + grpc_async_request(client::gRPCServiceClient{TRequest,false,TResponse,false}, request::TRequest) where {TRequest<:Any,TResponse<:Any} + +Initiate an asynchronous gRPC request: send the request to the server and then immediately return a `gRPCRequest` object without waiting for the response. +In order to wait on / retrieve the result once its ready, call `grpc_async_await`. +This is ideal when you need to send many requests in parallel and waiting on each response before sending the next request would things down. + +```julia +using gRPCClient + +grpc_init() + +# Include the generated bindings +include("test/gen/test/test_pb.jl") + +# Create a client bound to a specific RPC +client = TestService_TestRPC_Client("localhost", 8001) + +# Make a syncronous request and get back a TestResponse +response = grpc_sync_request(client, TestRequest(1, zeros(UInt64, 1))) +@info response + +# Make some async requests and await their TestResponse +requests = Vector{gRPCRequest}() +for i in 1:10 + push!( + requests, + grpc_async_request(client, TestRequest(1, zeros(UInt64, 1))) + ) +end + +for request in requests + response = grpc_async_await(client, request) + @info response +end +``` +""" +function grpc_async_request( + client::gRPCServiceClient{TRequest,false,TResponse,false}, + request::TRequest, +) where {TRequest<:Any,TResponse<:Any} + + request_buf = grpc_encode_request_iobuffer( + request; + max_send_message_length = client.max_send_message_length, + ) + seekstart(request_buf) + + req = gRPCRequest( + client.grpc, + url(client), + request_buf, + IOBuffer(), + nothing, + nothing; + deadline = client.deadline, + keepalive = client.keepalive, + max_send_message_length = client.max_send_message_length, + max_recieve_message_length = client.max_recieve_message_length, + ) + + req +end + + +mutable struct gRPCAsyncChannelResponse{TResponse} + index::Int64 + response::Union{Nothing,TResponse} + ex::Union{Nothing,Exception} +end + +""" + grpc_async_request(client::gRPCServiceClient{TRequest,false,TResponse,false}, request::TRequest, channel::Channel{gRPCAsyncChannelResponse{TResponse}}, index::Int64) where {TRequest<:Any,TResponse<:Any} + +Initiate an asynchronous gRPC request: send the request to the server and then immediately return. When the request is complete a background task will put the response in the provided channel. +This has the advantage over the request / await patern in that you can handle responses immediately after they are recieved in any order. + +```julia +using gRPCClient + +grpc_init() +include("test/gen/test/test_pb.jl") + +# Connect to the test server +client = TestService_TestRPC_Client("localhost", 8001) + +N = 10 + +channel = Channel{gRPCAsyncChannelResponse{TestResponse}}(N) + +for (index, request) in enumerate([TestRequest(i, zeros(UInt64, i)) for i in 1:N]) + grpc_async_request(client, request, channel, index) +end + +for i in 1:N + cr = take!(channel) + # Check if an exception was thrown, if so throw it here + !isnothing(cr.ex) && throw(cr.ex) + + # If this does not hold true, then the requests and responses have gotten mixed up. + @assert length(cr.response.data) == cr.index +end + +``` +""" +function grpc_async_request( + client::gRPCServiceClient{TRequest,false,TResponse,false}, + request::TRequest, + channel::Channel{gRPCAsyncChannelResponse{TResponse}}, + index::Int64, +) where {TRequest<:Any,TResponse<:Any} + + request_buf = grpc_encode_request_iobuffer( + request; + max_send_message_length = client.max_send_message_length, + ) + seekstart(request_buf) + + req = gRPCRequest( + client.grpc, + url(client), + request_buf, + IOBuffer(), + nothing, + nothing; + deadline = client.deadline, + keepalive = client.keepalive, + max_send_message_length = client.max_send_message_length, + max_recieve_message_length = client.max_recieve_message_length, + ) + + Threads.@spawn begin + try + response = grpc_async_await(client, req) + put!(channel, gRPCAsyncChannelResponse{TResponse}(index, response, nothing)) + catch ex + put!(channel, gRPCAsyncChannelResponse{TResponse}(index, nothing, ex)) + end + end + + nothing +end + + +""" + grpc_async_await(client::gRPCServiceClient{TRequest,false,TResponse,false}, request::gRPCRequest) where {TRequest<:Any,TResponse<:Any} + +Wait for the request to complete and return the response when it is ready. Throws any exceptions that were encountered during handling of the request. +""" +grpc_async_await( + client::gRPCServiceClient{TRequest,false,TResponse,false}, + request::gRPCRequest, +) where {TRequest<:Any,TResponse<:Any} = grpc_async_await(request, TResponse) + + +""" + grpc_sync_request(client::gRPCServiceClient{TRequest,false,TResponse,false}, request::TRequest) where {TRequest<:Any,TResponse<:Any} + +Do a synchronous gRPC request: send the request and wait for the response before returning it. +Under the hood this just calls `grpc_async_request` and `grpc_async_await` +""" +grpc_sync_request( + client::gRPCServiceClient{TRequest,false,TResponse,false}, + request::TRequest, +) where {TRequest<:Any,TResponse<:Any} = + grpc_async_await(grpc_async_request(client, request), TResponse) diff --git a/src/Utils.jl b/src/Utils.jl new file mode 100644 index 0000000..7ae2e05 --- /dev/null +++ b/src/Utils.jl @@ -0,0 +1,13 @@ +isfull(ch::Channel) = begin + if ch.sz_max === 0 + isready(ch) + else + length(ch.data) ≥ ch.sz_max + end +end + +function nullstring(x::Vector{UInt8}) + first_zero_idx = findfirst(==(0), x) + isnothing(first_zero_idx) && return "" + String(x[1:first_zero_idx-1]) +end diff --git a/src/curl.jl b/src/curl.jl deleted file mode 100644 index 6d13af0..0000000 --- a/src/curl.jl +++ /dev/null @@ -1,322 +0,0 @@ -const GRPC_STATIC_HEADERS = Ref{Ptr{Nothing}}(C_NULL) - -const StatusCode = ( - OK = (code=0, message="Success"), - CANCELLED = (code=1, message="The operation was cancelled"), - UNKNOWN = (code=2, message="Unknown error"), - INVALID_ARGUMENT = (code=3, message="Client specified an invalid argument"), - DEADLINE_EXCEEDED = (code=4, message="Deadline expired before the operation could complete"), - NOT_FOUND = (code=5, message="Requested entity was not found"), - ALREADY_EXISTS = (code=6, message="Entity already exists"), - PERMISSION_DENIED = (code=7, message="No permission to execute the specified operation"), - RESOURCE_EXHAUSTED = (code=8, message="Resource exhausted"), - FAILED_PRECONDITION = (code=9, message="Operation was rejected because the system is not in a state required for the operation's execution"), - ABORTED = (code=10, message="Operation was aborted"), - OUT_OF_RANGE = (code=11, message="Operation was attempted past the valid range"), - UNIMPLEMENTED = (code=12, message="Operation is not implemented or is not supported/enabled in this service"), - INTERNAL = (code=13, message="Internal error"), - UNAVAILABLE = (code=14, message="The service is currently unavailable"), - DATA_LOSS = (code=15, message="Unrecoverable data loss or corruption"), - UNAUTHENTICATED = (code=16, message="The request does not have valid authentication credentials for the operation") -) - -grpc_status_info(code) = StatusCode[code+1] -grpc_status_message(code) = (grpc_status_info(code)).message -grpc_status_code_str(code) = string(propertynames(StatusCode)[code+1]) - -#= -const SEND_BUFFER_SZ = 1024 * 1024 -function buffer_send_data(input::Channel{T}) where T <: ProtoType - data = nothing - if isready(input) - iob = IOBuffer() - while isready(input) && (iob.size < SEND_BUFFER_SZ) - write(iob, to_delimited_message_bytes(take!(input))) - yield() - end - data = take!(iob) - elseif isopen(input) - data = UInt8[] - end - data -end -=# - -function share_lock(easy_p::Ptr{Cvoid}, data::curl_lock_data, access::curl_lock_access, userptr::Ptr{Cvoid}) - share = unsafe_pointer_to_objref(Ptr{CurlShare}(userptr))::CurlShare - lock(share.locks[data]) - nothing -end - -function share_unlock(easy_p::Ptr{Cvoid}, data::curl_lock_data, userptr::Ptr{Cvoid}) - share = unsafe_pointer_to_objref(Ptr{CurlShare}(userptr))::CurlShare - unlock(share.locks[data]) - nothing -end - -mutable struct CurlShare - shptr::Ptr{CURLSH} - locks::Vector{ReentrantLock} - closed::Bool - - function CurlShare() - shptr = curl_share_init() - curl_share_setopt(shptr, CURLSHOPT_SHARE, CURL_LOCK_DATA_SHARE) - curl_share_setopt(shptr, CURLSHOPT_SHARE, CURL_LOCK_DATA_COOKIE) - curl_share_setopt(shptr, CURLSHOPT_SHARE, CURL_LOCK_DATA_DNS) - curl_share_setopt(shptr, CURLSHOPT_SHARE, CURL_LOCK_DATA_PSL) - curl_share_setopt(shptr, CURLSHOPT_SHARE, CURL_LOCK_DATA_CONNECT) - - share_lock_cb = @cfunction(share_lock, Cvoid, (Ptr{Cvoid}, Cuint, Cuint, Ptr{Cvoid})) - share_unlock_cb = @cfunction(share_unlock, Cvoid, (Ptr{Cvoid}, Cuint, Ptr{Cvoid})) - - @ccall LibCURL.LibCURL_jll.libcurl.curl_share_setopt(shptr::Ptr{CURLSH}, CURLSHOPT_LOCKFUNC::CURLSHoption; share_lock_cb::Ptr{Cvoid})::CURLSHcode - @ccall LibCURL.LibCURL_jll.libcurl.curl_share_setopt(shptr::Ptr{CURLSH}, CURLSHOPT_UNLOCKFUNC::CURLSHoption; share_unlock_cb::Ptr{Cvoid})::CURLSHcode - - locks = Vector(undef, CURL_LOCK_DATA_LAST) - for idx in 1:CURL_LOCK_DATA_LAST - locks[idx] = ReentrantLock() - end - - obj = new(shptr, locks, false) - userptr = pointer_from_objref(obj) - @ccall LibCURL.LibCURL_jll.libcurl.curl_share_setopt(shptr::Ptr{CURLSH}, CURLSHOPT_USERDATA::CURLSHoption; userptr::Ptr{Cvoid})::CURLSHcode - obj - end -end - -function close(share::CurlShare) - if share.closed - curl_share_cleanup(share.shptr) - share.closed = true - end - nothing -end - -function send_data(easy::Curl.Easy, input::Channel{T}, max_send_message_length::Int) where T <: ProtoType - while true - yield() - data = isready(input) ? to_delimited_message_bytes(take!(input), max_send_message_length) : isopen(input) ? UInt8[] : nothing - easy.input === nothing && break - easy.input = data - Curl.curl_easy_pause(easy.handle, Curl.CURLPAUSE_CONT) - wait(easy.ready) - easy.input === nothing && break - easy.ready = Threads.Event() - end -end - -function grpc_timeout_header_val(timeout::Real) - if round(Int, timeout) == timeout - timeout_secs = round(Int64, timeout) - return "$(timeout_secs)S" - end - timeout *= 1000 - if round(Int, timeout) == timeout - timeout_millisecs = round(Int64, timeout) - return "$(timeout_millisecs)m" - end - timeout *= 1000 - if round(Int, timeout) == timeout - timeout_microsecs = round(Int64, timeout) - return "$(timeout_microsecs)u" - end - timeout *= 1000 - timeout_nanosecs = round(Int64, timeout) - return "$(timeout_nanosecs)n" -end - -function grpc_headers(; timeout::Real=Inf) - headers = C_NULL - headers = LibCURL.curl_slist_append(headers, "User-Agent: $(Curl.USER_AGENT)") - headers = LibCURL.curl_slist_append(headers, "Content-Type: application/grpc+proto") - headers = LibCURL.curl_slist_append(headers, "Content-Length:") - headers = LibCURL.curl_slist_append(headers, "te: trailers") - if timeout !== Inf - headers = LibCURL.curl_slist_append(headers, "grpc-timeout: $(grpc_timeout_header_val(timeout))") - end - headers -end - -function grpc_request_header(request_timeout::Real) - if request_timeout == Inf - GRPC_STATIC_HEADERS[] - else - grpc_headers(; timeout=request_timeout) - end -end - -function easy_handle(curlshare::Union{Nothing,Ptr{CURLSH}}, maxage::Clong, keepalive::Clong, negotiation::Symbol, revocation::Bool, request_timeout::Real) - easy = Curl.Easy() - http_version = (negotiation === :http2) ? CURL_HTTP_VERSION_2_0 : - (negotiation === :http2_tls) ? CURL_HTTP_VERSION_2TLS : - (negotiation === :http2_prior_knowledge) ? CURL_HTTP_VERSION_2_PRIOR_KNOWLEDGE : - throw(ArgumentError("unsupported HTTP2 negotiation mode $negotiation")) - Curl.setopt(easy, CURLOPT_HTTP_VERSION, http_version) - Curl.setopt(easy, CURLOPT_PIPEWAIT, Clong(1)) - Curl.setopt(easy, CURLOPT_POST, Clong(1)) - Curl.setopt(easy, CURLOPT_HTTPHEADER, grpc_request_header(request_timeout)) - if curlshare !== nothing - Curl.setopt(easy, CURLOPT_SHARE, curlshare) - end - if !revocation - Curl.setopt(easy, CURLOPT_SSL_OPTIONS, CURLSSLOPT_NO_REVOKE) - end - if maxage > 0 - Curl.setopt(easy, CURLOPT_MAXAGE_CONN, maxage) - end - if keepalive > 0 - Curl.setopt(easy, CURLOPT_TCP_KEEPALIVE, Clong(1)) - Curl.setopt(easy, CURLOPT_TCP_KEEPINTVL, keepalive); - Curl.setopt(easy, CURLOPT_TCP_KEEPIDLE, keepalive); - end - easy -end - -function recv_data(easy::Curl.Easy, output::Channel{T}, max_recv_message_length::Int) where T <: ProtoType - iob = PipeBuffer() - waiting_for_header = true - msgsize = 0 - compressed = UInt8(0) - datalen = UInt32(0) - need_more = true - for buf in easy.output - write(iob, buf) - need_more = false - while !need_more - if waiting_for_header - if bytesavailable(iob) >= 5 - compressed = read(iob, UInt8) # compression - datalen = ntoh(read(iob, UInt32)) # message length - - if datalen > max_recv_message_length - throw(gRPCMessageTooLargeException(max_recv_message_length, datalen)) - end - - waiting_for_header = false - else - need_more = true - end - end - - if !waiting_for_header - if bytesavailable(iob) >= datalen - msgbytes = IOBuffer(view(iob.data, iob.ptr:(iob.ptr+datalen-1))) - put!(output, readproto(msgbytes, T())) # decode message bytes - iob.ptr += datalen - waiting_for_header = true - else - need_more = true - end - end - end - end - close(output) -end - -function set_connect_timeout(easy::Curl.Easy, timeout::Real) - maxval = typemax(Clong) ÷ 1000 - (0 ≤ timeout ≤ maxval) || - throw(ArgumentError("timeout value must be between 0 and $maxval, got $timeout")) - timeout_ms = round(Clong, timeout * 1000) - Curl.setopt(easy, CURLOPT_CONNECTTIMEOUT_MS, timeout_ms) -end - -# Prevent reuse of this handle -# Should be called if an error is detected and/or the server is likely to close connection -forbid_reuse(easy::Curl.Easy) = Curl.setopt(easy, CURLOPT_FORBID_REUSE, Clong(1)) - -function get_grpc_status(easy::Curl.Easy) - grpc_status = StatusCode.OK.code - grpc_message = "" - - # parse the grpc headers - @debug("response headers", easy.res_hdrs) - for hdr in easy.res_hdrs - if startswith(hdr, "grpc-status") - grpc_status = parse(Int, strip(last(split(hdr, ':'; limit=2)))) - elseif startswith(hdr, "grpc-message") - grpc_message = string(strip(last(split(hdr, ':'; limit=2)))) - end - end - - if (easy.code == CURLE_OPERATION_TIMEDOUT) && (grpc_status == StatusCode.OK.code) - grpc_status = StatusCode.DEADLINE_EXCEEDED.code - end - if (grpc_status != StatusCode.OK.code) && isempty(grpc_message) - grpc_message = grpc_status_message(grpc_status) - end - - return grpc_status, grpc_message -end - -function grpc_request(curlshare::Union{Nothing,Ptr{CURLSH}}, downloader::Downloader, url::String, input::Channel{T1}, output::Channel{T2}; - maxage::Clong = typemax(Clong), - keepalive::Clong = 60, - negotiation::Symbol = :http2_prior_knowledge, - revocation::Bool = true, - request_timeout::Real = Inf, - connect_timeout::Real = 0, - max_recv_message_length::Int = DEFAULT_MAX_RECV_MESSAGE_LENGTH, - max_send_message_length::Int = DEFAULT_MAX_SEND_MESSAGE_LENGTH, - verbose::Bool = false)::gRPCStatus where {T1 <: ProtoType, T2 <: ProtoType} - Curl.with_handle(easy_handle(curlshare, maxage, keepalive, negotiation, revocation, request_timeout)) do easy - # setup the request - Curl.set_url(easy, url) - Curl.set_timeout(easy, request_timeout) - set_connect_timeout(easy, connect_timeout) - Curl.set_verbose(easy, verbose) - if isdefined(Curl, :add_upload_callbacks) - Curl.add_upload_callbacks(easy) - else - Curl.add_upload_callback(easy) - end - Downloads.set_ca_roots(downloader, easy) - - # do the request - Curl.add_handle(downloader.multi, easy) - - function cleanup() - Curl.remove_handle(downloader.multi, easy) - # though remove_handle sets easy.handle to C_NULL, it does not close output and progress channels - # we need to close them here to unblock anything waiting on them - close(easy.output) - close(easy.progress) - close(output) - close(input) - nothing - end - - exception = nothing - grpc_status = StatusCode.OK.code - grpc_message = "" - - # do send recv data - try - Base.Experimental.@sync begin - @async recv_data(easy, output, max_recv_message_length) - @async send_data(easy, input, max_send_message_length) - end - grpc_status, grpc_message = get_grpc_status(easy) - if ((easy.code != CURLE_OK) || (grpc_status != StatusCode.OK.code)) - forbid_reuse(easy) - end - catch ex - forbid_reuse(easy) - exception = ex - finally # ensure handle is removed - cleanup() - end - - # throw the unwrapped exception if there was one - if exception !== nothing - throw(exception) - end - - if ((easy.code == CURLE_OK) && (grpc_status == StatusCode.OK.code)) - gRPCStatus(true, grpc_status, "") - else - gRPCStatus(false, grpc_status, isempty(grpc_message) ? Curl.get_curl_errstr(easy) : grpc_message) - end - end -end diff --git a/src/gRPC.jl b/src/gRPC.jl new file mode 100644 index 0000000..e14e98b --- /dev/null +++ b/src/gRPC.jl @@ -0,0 +1,164 @@ +const _grpc = gRPCCURL() + +""" + grpc_global_handle() + +Returns the global `gRPCCURL` state which contains a libCURL multi handle. By default all gRPC clients use this multi in order to ensure that HTTP/2 multiplexing happens where possible. +""" +grpc_global_handle() = _grpc + +""" + grpc_init([grpc_curl::gRPCCURL]) + +Initializes the `gRPCCURL` object. This should be called once before making gRPC calls. There is no harm in calling this more than once (ie by different packages/dependencies). Typical usage looks like this: + +```julia +grpc_init() + +client = TestService_TestRPC_Client("172.238.177.88", 8001) + +# Make some gRPC calls + +# Shut down the global gRPC handle +grpc_shutdown() +``` + +Unless specifying a `gRPCCURL` the global one provided by `grpc_global_handle()` is used. Each `gRPCCURL` state has its own connection pool and request semaphore, so sometimes you may want to manage your own like shown below: + +```julia +grpc_myapp = gRPCCURL() +grpc_init(grpc_myapp) + +client = TestService_TestRPC_Client("172.238.177.88", 8001; grpc=grpc_myapp) + +# Make some gRPC calls + +# Only shuts down your gRPC handle +grpc_shutdown(grpc_myapp) +``` +""" +grpc_init() = open(grpc_global_handle()) +grpc_init(grpc_curl::gRPCCURL) = open(grpc_curl) + +""" + grpc_shutdown([grpc_curl::gRPCCURL]) + +Shuts down the `gRPCCURL`. This neatly cleans up all active connections and requests. Useful for calling during development with Revise. Unless specifying the `gRPCCURL`, the global one provided by `grpc_global_handle()` is shutdown. +""" +grpc_shutdown() = close(grpc_global_handle()) +grpc_shutdown(grpc_curl::gRPCCURL) = close(grpc_curl) + + +struct gRPCServiceClient{TRequest,SRequest,TResponse,SResponse} + grpc::gRPCCURL + host::String + port::Int64 + path::String + secure::Bool + deadline::Float64 + keepalive::Float64 + max_send_message_length::Int64 + max_recieve_message_length::Int64 + + function gRPCServiceClient{TRequest,SRequest,TResponse,SResponse}( + host, + port, + path; + secure = false, + grpc = grpc_global_handle(), + deadline = 10, + keepalive = 60, + max_send_message_length = 4 * 1024 * 1024, + max_recieve_message_length = 4 * 1024 * 1024, + ) where {TRequest<:Any,SRequest,TResponse<:Any,SResponse} + new( + grpc, + host, + port, + path, + secure, + deadline, + keepalive, + max_send_message_length, + max_recieve_message_length, + ) + end + +end + +function url(client::gRPCServiceClient) + protocol = if client.secure + "grpcs" + else + "grpc" + end + "$protocol://$(client.host):$(client.port)$(client.path)" +end + + +function grpc_encode_request_iobuffer( + request, + req_buf::IOBuffer; + max_send_message_length = 4 * 1024 * 1024, +) + start_pos = position(req_buf) + + # Write compressed flag and length prefix + write(req_buf, UInt8(0)) + write(req_buf, UInt32(0)) + + # Serialize the protobuf + e = ProtoEncoder(req_buf) + sz = UInt32(encode(e, request)) + + end_pos = position(req_buf) + + if req_buf.size - GRPC_HEADER_SIZE > max_send_message_length + throw( + gRPCServiceCallException( + GRPC_RESOURCE_EXHAUSTED, + "request message larger than max_send_message_length: $(req_buf.size - GRPC_HEADER_SIZE) > $max_send_message_length", + ), + ) + end + + # Seek back to length prefix and update it with size of encoded protobuf + seek(req_buf, start_pos + 1) + write(req_buf, hton(sz)) + + # Seek back to the end + seek(req_buf, end_pos) + + req_buf +end + + +grpc_encode_request_iobuffer(request; max_send_message_length = 4 * 1024 * 1024) = + grpc_encode_request_iobuffer( + request, + IOBuffer(); + max_send_message_length = max_send_message_length, + ) + + +function grpc_async_await(req::gRPCRequest) + # Wait for request to be done + wait(req) + + # Throw an exception for this request if we have one + !isnothing(req.ex) && throw(req.ex) + + req.grpc_status != GRPC_OK && + throw(gRPCServiceCallException(req.grpc_status, req.grpc_message)) + + req.code == CURLE_OPERATION_TIMEDOUT && + throw(gRPCServiceCallException(GRPC_DEADLINE_EXCEEDED, "Deadline exceeded.")) + req.code != CURLE_OK && + throw(gRPCServiceCallException(GRPC_INTERNAL, nullstring(req.errbuf))) +end + + +function grpc_async_await(req::gRPCRequest, TResponse) + grpc_async_await(req) + return decode(ProtoDecoder(req.response), TResponse) +end diff --git a/src/gRPCClient.jl b/src/gRPCClient.jl index d7d2daf..46b9d7c 100644 --- a/src/gRPCClient.jl +++ b/src/gRPCClient.jl @@ -1,24 +1,170 @@ module gRPCClient +using PrecompileTools: @setup_workload, @compile_workload + using LibCURL -using Downloads +using Base.Threads using ProtoBuf +using FileWatching +using Base: OS_HANDLE +using Base: Semaphore, acquire, release +using Base.Threads -import Downloads: Curl -import ProtoBuf: call_method -import Base: close +import Base.wait, + Base.reset, Base.notify, Base.isreadable, Base.iswritable, Base.close, Base.open +import ProtoBuf.CodeGenerators.ServiceType, + ProtoBuf.CodeGenerators.Context, ProtoBuf.CodeGenerators.register_service_codegen -export gRPCController, gRPCChannel, gRPCException, gRPCServiceCallException, gRPCMessageTooLargeException, gRPCStatus, gRPCCheck, StatusCode abstract type gRPCException <: Exception end -include("limitio.jl") -include("curl.jl") -include("grpc.jl") -include("generate.jl") +""" +Exception type that is thrown when something goes wrong while calling an RPC. This can either be triggered by the servers response code or by the client when something fails. + +This exception type has two fields: + +1. `grpc_status::Int` - See [here](https://grpc.io/docs/guides/status-codes/) for an indepth explanation of each status. +2. `message::String` + +""" +struct gRPCServiceCallException <: gRPCException + grpc_status::Int + message::String +end + +const GRPC_HEADER_SIZE = 5 +const GRPC_MAX_STREAMS = 16 + +const GRPC_OK = 0 +const GRPC_CANCELLED = 1 +const GRPC_UNKNOWN = 2 +const GRPC_INVALID_ARGUMENT = 3 +const GRPC_DEADLINE_EXCEEDED = 4 +const GRPC_NOT_FOUND = 5 +const GRPC_ALREADY_EXISTS = 6 +const GRPC_PERMISSION_DENIED = 7 +const GRPC_RESOURCE_EXHAUSTED = 8 +const GRPC_FAILED_PRECONDITION = 9 +const GRPC_ABORTED = 10 +const GRPC_OUT_OF_RANGE = 11 +const GRPC_UNIMPLEMENTED = 12 +const GRPC_INTERNAL = 13 +const GRPC_UNAVAILABLE = 14 +const GRPC_DATA_LOSS = 15 +const GRPC_UNAUTHENTICATED = 16 + +const GRPC_CODE_TABLE = Dict{Int64,String}( + 0 => "OK", + 1 => "CANCELLED", + 2 => "UNKNOWN", + 3 => "INVALID_ARGUMENT", + 4 => "DEADLINE_EXCEEDED", + 5 => "NOT_FOUND", + 6 => "ALREADY_EXISTS", + 7 => "PERMISSION_DENIED", + 8 => "RESOURCE_EXHAUSTED", + 9 => "FAILED_PRECONDITION", + 10 => "ABORTED", + 11 => "OUT_OF_RANGE", + 12 => "UNIMPLEMENTED", + 13 => "INTERNAL", + 14 => "UNAVAILABLE", + 15 => "DATA_LOSS", + 16 => "UNAUTHENTICATED", +) -function __init__() - GRPC_STATIC_HEADERS[] = grpc_headers() +function Base.showerror(io::IO, e::gRPCServiceCallException) + print( + io, + "gRPCServiceCallException(grpc_status=$(GRPC_CODE_TABLE[e.grpc_status])($(e.grpc_status)), message=\"$(e.message)\")", + ) end -end # module +include("Utils.jl") +include("Curl.jl") +include("gRPC.jl") +include("Unary.jl") +include("Streaming.jl") +include("ProtoBuf.jl") + +export grpc_init +export grpc_shutdown +export grpc_global_handle +export grpc_register_service_codegen + +export grpc_async_request +export grpc_async_await +export grpc_sync_request + +export gRPCCURL +export gRPCRequest +export gRPCServiceClient +export gRPCAsyncChannelResponse + +export gRPCException +export gRPCServiceCallException + + +@setup_workload begin + # We don't have a Julia gRPC server so call my Linode's public gRPC endpoint + TEST_HOST = "172.238.177.88" + # TODO: change this to port 80 to fix issues with corporate firewalls + TEST_PORT = 8001 + + @compile_workload begin + include("../test/gen/test/test_pb.jl") + + # Initialize the gRPC package - grpc_shutdown() does the opposite for use with Revise. + grpc_init() + + # Unary + client_unary = TestService_TestRPC_Client(TEST_HOST, TEST_PORT) + + # Sync API + test_response = grpc_sync_request(client_unary, TestRequest(1, Vector{UInt64}())) + + # Async API + request = grpc_async_request(client_unary, TestRequest(1, Vector{UInt64}())) + test_response = grpc_async_await(client_unary, request) + + # Streaming + @static if VERSION >= v"1.12" + + # Request + client_request = TestService_TestClientStreamRPC_Client(TEST_HOST, TEST_PORT) + request_c = Channel{TestRequest}(16) + put!(request_c, TestRequest(1, zeros(UInt64, 1))) + close(request_c) + test_response = + grpc_async_await(client_request, grpc_async_request(client_request, request_c)) + + # Response + client_response = TestService_TestServerStreamRPC_Client(TEST_HOST, TEST_PORT) + response_c = Channel{TestResponse}(16) + req = grpc_async_request( + client_response, + TestRequest(1, zeros(UInt64, 1)), + response_c, + ) + test_response = take!(response_c) + grpc_async_await(req) + + # Bidirectional + client_bidirectional = + TestService_TestBidirectionalStreamRPC_Client(TEST_HOST, TEST_PORT) + request_c = Channel{TestRequest}(16) + response_c = Channel{TestResponse}(16) + put!(request_c, TestRequest(1, zeros(UInt64, 1))) + req = grpc_async_request(client_bidirectional, request_c, response_c) + test_response = take!(response_c) + close(request_c) + grpc_async_await(req) + end + + grpc_shutdown() + end +end + + + +end diff --git a/src/generate.jl b/src/generate.jl deleted file mode 100644 index b6f3bdd..0000000 --- a/src/generate.jl +++ /dev/null @@ -1,172 +0,0 @@ -const package_regex = r"package\s(\S*)[\s]*;.*" -const service_regex = r"service\s(\S*)[\s]*.*" - -function write_header(io, generated_module, package, client_module_name) - print(io, """module $(client_module_name) - using gRPCClient - - include("$(generated_module).jl") - using .$(package) - - import Base: show - """) -end - -function write_trailer(io, client_module_name) - print(io, """ - - end # module $(client_module_name) - """) -end - -function write_service(io, package, service, methods) - print(io, """ - - # begin service: $(package).$(service) - - export $(service)BlockingClient, $(service)Client - - struct $(service)BlockingClient - controller::gRPCController - channel::gRPCChannel - stub::$(service)BlockingStub - - function $(service)BlockingClient(api_base_url::String; kwargs...) - controller = gRPCController(; kwargs...) - channel = gRPCChannel(api_base_url) - stub = $(service)BlockingStub(channel) - new(controller, channel, stub) - end - end - - struct $(service)Client - controller::gRPCController - channel::gRPCChannel - stub::$(service)Stub - - function $(service)Client(api_base_url::String; kwargs...) - controller = gRPCController(; kwargs...) - channel = gRPCChannel(api_base_url) - stub = $(service)Stub(channel) - new(controller, channel, stub) - end - end - - show(io::IO, client::$(service)BlockingClient) = print(io, "$(service)BlockingClient(", client.channel.baseurl, ")") - show(io::IO, client::$(service)Client) = print(io, "$(service)Client(", client.channel.baseurl, ")") - """) - - for method in methods - write_service_method(io, package, service, method) - end - - print(io, """ - - # end service: $(package).$(service) - """) -end - -typename(ch::Type{T}) where {T <: Channel} = string("Channel{", typename(eltype(ch)), "}") -typename(T) = last(split(string(T), '.'; limit=2)) - -function write_service_method(io, package, service, method) - method_name = method.name - input_type = typename(method.input_type) - output_type = typename(method.output_type) - - print(io, """ - - import .$(package): $(method_name) - \"\"\" - $(method_name) - - - input: $input_type - - output: $output_type - \"\"\" - $(method_name)(client::$(service)BlockingClient, inp::$(input_type)) = $(method_name)(client.stub, client.controller, inp) - $(method_name)(client::$(service)Client, inp::$(input_type), done::Function) = $(method_name)(client.stub, client.controller, inp, done) - """) -end - -function detect_services(proto::String) - package = "" - services = String[] - - for line in readlines(proto) - line = strip(line) - if startswith(line, "package") - regexmatches = match(package_regex, line) - if (regexmatches !== nothing) && (length(regexmatches.captures) == 1) - package = string(first(regexmatches.captures)) - end - elseif startswith(line, "service") - regexmatches = match(service_regex, line) - if (regexmatches !== nothing) && (length(regexmatches.captures) == 1) - service = string(first(regexmatches.captures)) - push!(services, service) - end - end - end - package, services -end - -function get_generated_method_table(s::String) - T = Main - for t in split(s, ".") - T = Base.eval(T, Symbol(t)) - end - T -end - -""" - generate(proto::String; outdir::String=pwd()) - -Generate a gRPC client from protobuf specification file. - -- `proto`: Path to the protobuf specification to used. -- `outdir`: Directory to write generated code into, created if not present - already. Existing files if any will be overwtitten. -""" -function generate(proto::String; outdir::String=pwd(), includes::Vector{String}=String[]) - if !isfile(proto) - throw(ArgumentError("No such file - $proto")) - end - proto = abspath(proto) - - @info("Generating gRPC client", proto, outdir) - - # determine the package name and service name - package, services = detect_services(proto) - protodir = dirname(proto) - includeflag = `-I=$protodir` - for inc in includes - includeflag = `$includeflag -I=$inc` - end - @info("Detected", package, services, includes) - - # generate protobuf services - mkpath(outdir) - bindir = Sys.BINDIR - pathenv = string(ENV["PATH"], Sys.iswindows() ? ";" : ":", bindir) - withenv("PATH"=>pathenv) do - ProtoBuf.protoc(`$includeflag --julia_out=$outdir $proto`) - end - - # include the generated code and detect service method names - generated_module = first(split(package, '.'; limit=2)) - generated_module_file = joinpath(outdir, string(generated_module, ".jl")) - Main.eval(:(include($generated_module_file))) - - # generate the gRPC client code - client_module_name = string(titlecase(generated_module; strict=false), "Clients") - open(joinpath(outdir, "$(client_module_name).jl"), "w") do grpcservice - write_header(grpcservice, generated_module, package, client_module_name) - for service in services - methods = get_generated_method_table(string(package, "._", service, "_methods")) - write_service(grpcservice, package, service, methods) - end - write_trailer(grpcservice, client_module_name) - end - - @info("Generated", outdir) -end diff --git a/src/grpc.jl b/src/grpc.jl deleted file mode 100644 index ba28a02..0000000 --- a/src/grpc.jl +++ /dev/null @@ -1,230 +0,0 @@ -""" - struct gRPCStatus - success::Bool - message::String - end - -`gRPCStatus` represents the status of a request. It has the following fields: - -- `success`: whether the request was completed successfully. -- `message`: any error message if request was not successful -""" -struct gRPCStatus - success::Bool - grpc_status::Int - message::String - exception::Union{Nothing,Exception} -end - -gRPCStatus(success::Bool, grpc_status::Int, message::AbstractString) = gRPCStatus(success, grpc_status, string(message), nothing) -function gRPCStatus(status_future) - try - return fetch(status_future) - catch ex - task_exception = isa(ex, TaskFailedException) ? ex.task.exception : ex - while isa(task_exception, TaskFailedException) - task_exception = task_exception.task.exception - end - return gRPCStatus(false, StatusCode.INTERNAL.code, string(task_exception), task_exception) - end -end - -""" - struct gRPCServiceCallException - message::String - end - -A `gRPCServiceCallException` is thrown if a gRPC request is not successful. -It has the following members: - -- `message`: any error message if request was not successful -""" -struct gRPCServiceCallException <: gRPCException - grpc_status::Int - message::String -end - -Base.show(io::IO, m::gRPCServiceCallException) = print(io, "gRPCServiceCallException: $(m.grpc_status), $(m.message)") - -""" - gRPCCheck(status; throw_error::Bool=true) - -Every gRPC request returns the result and a future representing the status -of the gRPC request. Check the response of a gRPC request and raise a -`gRPCException` if it has failed. If `throw_error` is set to false, this -returns `true` or `false` indicating success instead. -""" -gRPCCheck(status_future; throw_error::Bool=true) = gRPCCheck(gRPCStatus(status_future); throw_error=throw_error) -function gRPCCheck(status::gRPCStatus; throw_error::Bool=true) - if throw_error && !status.success - if status.exception === nothing - throw(gRPCServiceCallException(status.grpc_status, status.message)) - else - throw(status.exception) - end - end - status.success -end - -""" - gRPCController(; - [ maxage::Int = 0, ] - [ keepalive::Int64 = 60, ] - [ negotiation::Symbol = :http2_prior_knowledge, ] - [ revocation::Bool = true, ] - [ request_timeout::Real = Inf, ] - [ connect_timeout::Real = 0, ] - [ max_message_length = DEFAULT_MAX_MESSAGE_LENGTH, ] - [ max_recv_message_length = 0, ] - [ max_send_message_length = 0, ] - [ verbose::Bool = false, ] - ) - -Contains settings to control the behavior of gRPC requests. -- `maxage`: maximum age (seconds) of a connection beyond which it will not - be reused (default 180 seconds, same as setting this to 0). -- `keepalive`: interval (seconds) in which to send TCP keepalive messages on - the connection (default 60 seconds). -- `negotiation`: how to negotiate HTTP2, can be one of `:http2_prior_knowledge` - (no negotiation, the default), `:http2_tls` (http2 upgrade but only over - tls), or `:http2` (http2 upgrade) -- `revocation`: whether to check for certificate recovation (default is true) -- `request_timeout`: request timeout (seconds) -- `connect_timeout`: connect timeout (seconds) (must be ≤ typemax(Clong)÷1000, - default is 300 seconds, same as setting this to 0) -- `max_message_length`: maximum message length (default is 4MB) -- `max_recv_message_length`: maximum message length to receive (default is - `max_message_length`, same as setting this to 0) -- `max_send_message_length`: maximum message length to send (default is - `max_message_length`, same as setting this to 0) -- `enable_shared_locks`: whether to enable locks for using gRPCClient across - tasks/threads concurrently (experimental, default is false) -- `verbose`: whether to print out verbose communication logs (default false) -""" -struct gRPCController <: ProtoRpcController - maxage::Clong - keepalive::Clong - negotiation::Symbol - revocation::Bool - request_timeout::Real - connect_timeout::Real - max_recv_message_length::Int - max_send_message_length::Int - enable_shared_locks::Bool - verbose::Bool - - function gRPCController(; - maxage::Integer = 0, - keepalive::Integer = 60, - negotiation::Symbol = :http2_prior_knowledge, - revocation::Bool = true, - request_timeout::Real = Inf, - connect_timeout::Real = 0, - max_message_length::Integer = DEFAULT_MAX_MESSAGE_LENGTH, - max_recv_message_length::Integer = 0, - max_send_message_length::Integer = 0, - enable_shared_locks::Bool = false, - verbose::Bool = false - ) - if maxage < 0 || keepalive < 0 || request_timeout < 0 || - connect_timeout < 0 || connect_timeout > (typemax(Clong) ÷ 1000) || - max_message_length < 0 || max_recv_message_length < 0 || max_send_message_length < 0 - throw(ArgumentError("Invalid gRPCController parameter")) - end - (max_recv_message_length == 0) && (max_recv_message_length = max_message_length) - (max_send_message_length == 0) && (max_send_message_length = max_message_length) - new(maxage, - keepalive, - negotiation, - revocation, - request_timeout, - connect_timeout, - max_recv_message_length, - max_send_message_length, - enable_shared_locks, - verbose, - ) - end -end - -""" - gRPCChannel(baseurl) - -`gRPCChannel` represents a connection to a specific service endpoint -(service `baseurl`) of a gRPC server. - -A channel also usually has a single network connection backing it and -multiple streams of requests can flow through it at any time. The number -of streams that can be multiplexed is negotiated between the client and -the server. -""" -struct gRPCChannel <: ProtoRpcChannel - downloader::Downloader - baseurl::String - curlshare::CurlShare - - function gRPCChannel(baseurl::String) - downloader = Downloader(; grace=Inf) - Curl.init!(downloader.multi) - Curl.setopt(downloader.multi, CURLMOPT_PIPELINING, CURLPIPE_MULTIPLEX) - endswith(baseurl, '/') && (baseurl = baseurl[1:end-1]) - new(downloader, baseurl, CurlShare()) - end -end - -function close(channel::gRPCChannel) - close(channel.curlshare) - nothing -end - -function to_delimited_message_bytes(msg, max_message_length::Int) - iob = IOBuffer() - limitiob = LimitIO(iob, max_message_length) - write(limitiob, UInt8(0)) # compression - write(limitiob, hton(UInt32(0))) # message length (placeholder) - data_len = writeproto(limitiob, msg) # message bytes - - seek(iob, 1) # seek out the message length placeholder - write(iob, hton(UInt32(data_len))) # fill the message length - take!(iob) -end - -function call_method(channel::gRPCChannel, service::ServiceDescriptor, method::MethodDescriptor, controller::gRPCController, request::T) where T <: ProtoType - inputchannel = Channel{T}(1) - put!(inputchannel, request) - close(inputchannel) - call_method(channel, service, method, controller, inputchannel) -end -call_method(channel::gRPCChannel, service::ServiceDescriptor, method::MethodDescriptor, controller::gRPCController, input::Channel{T}) where T <: ProtoType = call_method(channel, service, method, controller, input, get_response_type(method)) -function call_method(channel::gRPCChannel, service::ServiceDescriptor, method::MethodDescriptor, controller::gRPCController, input::Channel{T1}, ::Type{Channel{T2}}) where {T1 <: ProtoType, T2 <: ProtoType} - call_method(channel, service, method, controller, input, Channel{T2}()) -end -function call_method(channel::gRPCChannel, service::ServiceDescriptor, method::MethodDescriptor, controller::gRPCController, input::Channel{T1}, ::Type{T2}) where {T1 <: ProtoType, T2 <: ProtoType} - outchannel, status_future = call_method(channel, service, method, controller, input, Channel{T2}()) - try - return (take!(outchannel), status_future) - catch ex - gRPCCheck(status_future) # check for core issue - if isa(ex, InvalidStateException) - throw(gRPCServiceCallException("Server closed connection without any response")) - else - rethrow() # throw this error if there's no other issue - end - end -end -function call_method(channel::gRPCChannel, service::ServiceDescriptor, method::MethodDescriptor, controller::gRPCController, input::Channel{T1}, outchannel::Channel{T2}) where {T1 <: ProtoType, T2 <: ProtoType} - url = string(channel.baseurl, "/", service.name, "/", method.name) - shptr = controller.enable_shared_locks ? channel.curlshare.shptr : nothing - status_future = @async grpc_request(shptr, channel.downloader, url, input, outchannel; - maxage = controller.maxage, - keepalive = controller.keepalive, - negotiation = controller.negotiation, - revocation = controller.revocation, - request_timeout = controller.request_timeout, - connect_timeout = controller.connect_timeout, - max_recv_message_length = controller.max_recv_message_length, - max_send_message_length = controller.max_send_message_length, - verbose = controller.verbose, - ) - outchannel, status_future -end diff --git a/src/limitio.jl b/src/limitio.jl deleted file mode 100644 index 31caf16..0000000 --- a/src/limitio.jl +++ /dev/null @@ -1,48 +0,0 @@ -# limits number of bytes written to an io stream (originally from https://github.com/JuliaDebug/Debugger.jl/blob/master/src/limitio.jl) -# useful to detect messages that would go over limit when converted to bytes. -mutable struct LimitIO{IO_t <: IO} <: IO - io::IO_t - maxbytes::Int - n::Int # max bytes to write -end -LimitIO(io::IO, maxbytes) = LimitIO(io, maxbytes, 0) - -function Base.write(io::LimitIO, v::UInt8) - io.n > io.maxbytes && throw(gRPCMessageTooLargeException(io.maxbytes, io.n)) - nincr = write(io.io, v) - io.n += nincr - nincr -end - -""" -Default maximum gRPC message size -""" -const DEFAULT_MAX_MESSAGE_LENGTH = 1024*1024*4 -const DEFAULT_MAX_RECV_MESSAGE_LENGTH = DEFAULT_MAX_MESSAGE_LENGTH -const DEFAULT_MAX_SEND_MESSAGE_LENGTH = DEFAULT_MAX_MESSAGE_LENGTH - -""" - struct gRPCMessageTooLargeException - limit::Int - encountered::Int - end - -A `gRPMessageTooLargeException` exception is thrown when a message is -encountered that has a size greater than the limit configured. -Specifically, `max_recv_message_length` while receiving and -`max_send_message_length` while sending. - -A `gRPMessageTooLargeException` has the following members: - -- `limit`: the limit value that was exceeded -- `encountered`: the amount of data that was actually received - or sent before this error was triggered. Note that this may - not correspond to the full size of the data, as error may be - thrown before actually materializing the complete data. -""" -struct gRPCMessageTooLargeException <: gRPCException - limit::Int - encountered::Int -end - -Base.show(io::IO, m::gRPCMessageTooLargeException) = print(io, "gRPMessageTooLargeException($(m.limit), $(m.encountered)) - Encountered message size $(m.encountered) > max configured $(m.limit)") \ No newline at end of file diff --git a/test/.gitignore b/test/.gitignore deleted file mode 100644 index 17bd12a..0000000 --- a/test/.gitignore +++ /dev/null @@ -1,5 +0,0 @@ -grpc-go -server.pid -routeguide_* -grpcerrors_* -testservers diff --git a/test/GrpcerrorsClients/GrpcerrorsClients.jl b/test/GrpcerrorsClients/GrpcerrorsClients.jl deleted file mode 100644 index f4625c2..0000000 --- a/test/GrpcerrorsClients/GrpcerrorsClients.jl +++ /dev/null @@ -1,84 +0,0 @@ -module GrpcerrorsClients -using gRPCClient - -include("grpcerrors.jl") -using .grpcerrors - -import Base: show - -# begin service: grpcerrors.GRPCErrors - -export GRPCErrorsBlockingClient, GRPCErrorsClient - -struct GRPCErrorsBlockingClient - controller::gRPCController - channel::gRPCChannel - stub::GRPCErrorsBlockingStub - - function GRPCErrorsBlockingClient(api_base_url::String; kwargs...) - controller = gRPCController(; kwargs...) - channel = gRPCChannel(api_base_url) - stub = GRPCErrorsBlockingStub(channel) - new(controller, channel, stub) - end -end - -struct GRPCErrorsClient - controller::gRPCController - channel::gRPCChannel - stub::GRPCErrorsStub - - function GRPCErrorsClient(api_base_url::String; kwargs...) - controller = gRPCController(; kwargs...) - channel = gRPCChannel(api_base_url) - stub = GRPCErrorsStub(channel) - new(controller, channel, stub) - end -end - -show(io::IO, client::GRPCErrorsBlockingClient) = print(io, "GRPCErrorsBlockingClient(", client.channel.baseurl, ")") -show(io::IO, client::GRPCErrorsClient) = print(io, "GRPCErrorsClient(", client.channel.baseurl, ")") - -import .grpcerrors: SimpleRPC -""" - SimpleRPC - -- input: grpcerrors.Data -- output: grpcerrors.Data -""" -SimpleRPC(client::GRPCErrorsBlockingClient, inp::grpcerrors.Data) = SimpleRPC(client.stub, client.controller, inp) -SimpleRPC(client::GRPCErrorsClient, inp::grpcerrors.Data, done::Function) = SimpleRPC(client.stub, client.controller, inp, done) - -import .grpcerrors: StreamResponse -""" - StreamResponse - -- input: grpcerrors.Data -- output: Channel{grpcerrors.Data} -""" -StreamResponse(client::GRPCErrorsBlockingClient, inp::grpcerrors.Data) = StreamResponse(client.stub, client.controller, inp) -StreamResponse(client::GRPCErrorsClient, inp::grpcerrors.Data, done::Function) = StreamResponse(client.stub, client.controller, inp, done) - -import .grpcerrors: StreamRequest -""" - StreamRequest - -- input: Channel{grpcerrors.Data} -- output: grpcerrors.Data -""" -StreamRequest(client::GRPCErrorsBlockingClient, inp::Channel{grpcerrors.Data}) = StreamRequest(client.stub, client.controller, inp) -StreamRequest(client::GRPCErrorsClient, inp::Channel{grpcerrors.Data}, done::Function) = StreamRequest(client.stub, client.controller, inp, done) - -import .grpcerrors: StreamRequestResponse -""" - StreamRequestResponse - -- input: Channel{grpcerrors.Data} -- output: Channel{grpcerrors.Data} -""" -StreamRequestResponse(client::GRPCErrorsBlockingClient, inp::Channel{grpcerrors.Data}) = StreamRequestResponse(client.stub, client.controller, inp) -StreamRequestResponse(client::GRPCErrorsClient, inp::Channel{grpcerrors.Data}, done::Function) = StreamRequestResponse(client.stub, client.controller, inp, done) - -# end service: grpcerrors.GRPCErrors - -end # module GrpcerrorsClients diff --git a/test/GrpcerrorsClients/grpcerrors.jl b/test/GrpcerrorsClients/grpcerrors.jl deleted file mode 100644 index 629b724..0000000 --- a/test/GrpcerrorsClients/grpcerrors.jl +++ /dev/null @@ -1,4 +0,0 @@ -module grpcerrors - const _ProtoBuf_Top_ = @static isdefined(parentmodule(@__MODULE__), :_ProtoBuf_Top_) ? (parentmodule(@__MODULE__))._ProtoBuf_Top_ : parentmodule(@__MODULE__) - include("grpcerrors_pb.jl") -end diff --git a/test/GrpcerrorsClients/grpcerrors_pb.jl b/test/GrpcerrorsClients/grpcerrors_pb.jl deleted file mode 100644 index a3303b8..0000000 --- a/test/GrpcerrorsClients/grpcerrors_pb.jl +++ /dev/null @@ -1,79 +0,0 @@ -# syntax: proto3 -using ProtoBuf -import ProtoBuf.meta - -mutable struct Data <: ProtoType - __protobuf_jl_internal_meta::ProtoMeta - __protobuf_jl_internal_values::Dict{Symbol,Any} - __protobuf_jl_internal_defaultset::Set{Symbol} - - function Data(; kwargs...) - obj = new(meta(Data), Dict{Symbol,Any}(), Set{Symbol}()) - values = obj.__protobuf_jl_internal_values - symdict = obj.__protobuf_jl_internal_meta.symdict - for nv in kwargs - fldname, fldval = nv - fldtype = symdict[fldname].jtyp - (fldname in keys(symdict)) || error(string(typeof(obj), " has no field with name ", fldname)) - if fldval !== nothing - values[fldname] = isa(fldval, fldtype) ? fldval : convert(fldtype, fldval) - end - end - obj - end -end # mutable struct Data -const __meta_Data = Ref{ProtoMeta}() -function meta(::Type{Data}) - ProtoBuf.metalock() do - if !isassigned(__meta_Data) - __meta_Data[] = target = ProtoMeta(Data) - allflds = Pair{Symbol,Union{Type,String}}[:mode => Int32, :param => Int32] - meta(target, Data, allflds, ProtoBuf.DEF_REQ, ProtoBuf.DEF_FNUM, ProtoBuf.DEF_VAL, ProtoBuf.DEF_PACK, ProtoBuf.DEF_WTYPES, ProtoBuf.DEF_ONEOFS, ProtoBuf.DEF_ONEOF_NAMES) - end - __meta_Data[] - end -end -function Base.getproperty(obj::Data, name::Symbol) - if name === :mode - return (obj.__protobuf_jl_internal_values[name])::Int32 - elseif name === :param - return (obj.__protobuf_jl_internal_values[name])::Int32 - else - getfield(obj, name) - end -end - -# service methods for GRPCErrors -const _GRPCErrors_methods = MethodDescriptor[ - MethodDescriptor("SimpleRPC", 1, Data, Data), - MethodDescriptor("StreamResponse", 2, Data, Channel{Data}), - MethodDescriptor("StreamRequest", 3, Channel{Data}, Data), - MethodDescriptor("StreamRequestResponse", 4, Channel{Data}, Channel{Data}) - ] # const _GRPCErrors_methods -const _GRPCErrors_desc = ServiceDescriptor("grpcerrors.GRPCErrors", 1, _GRPCErrors_methods) - -GRPCErrors(impl::Module) = ProtoService(_GRPCErrors_desc, impl) - -mutable struct GRPCErrorsStub <: AbstractProtoServiceStub{false} - impl::ProtoServiceStub - GRPCErrorsStub(channel::ProtoRpcChannel) = new(ProtoServiceStub(_GRPCErrors_desc, channel)) -end # mutable struct GRPCErrorsStub - -mutable struct GRPCErrorsBlockingStub <: AbstractProtoServiceStub{true} - impl::ProtoServiceBlockingStub - GRPCErrorsBlockingStub(channel::ProtoRpcChannel) = new(ProtoServiceBlockingStub(_GRPCErrors_desc, channel)) -end # mutable struct GRPCErrorsBlockingStub - -SimpleRPC(stub::GRPCErrorsStub, controller::ProtoRpcController, inp::Data, done::Function) = call_method(stub.impl, _GRPCErrors_methods[1], controller, inp, done) -SimpleRPC(stub::GRPCErrorsBlockingStub, controller::ProtoRpcController, inp::Data) = call_method(stub.impl, _GRPCErrors_methods[1], controller, inp) - -StreamResponse(stub::GRPCErrorsStub, controller::ProtoRpcController, inp::Data, done::Function) = call_method(stub.impl, _GRPCErrors_methods[2], controller, inp, done) -StreamResponse(stub::GRPCErrorsBlockingStub, controller::ProtoRpcController, inp::Data) = call_method(stub.impl, _GRPCErrors_methods[2], controller, inp) - -StreamRequest(stub::GRPCErrorsStub, controller::ProtoRpcController, inp::Channel{Data}, done::Function) = call_method(stub.impl, _GRPCErrors_methods[3], controller, inp, done) -StreamRequest(stub::GRPCErrorsBlockingStub, controller::ProtoRpcController, inp::Channel{Data}) = call_method(stub.impl, _GRPCErrors_methods[3], controller, inp) - -StreamRequestResponse(stub::GRPCErrorsStub, controller::ProtoRpcController, inp::Channel{Data}, done::Function) = call_method(stub.impl, _GRPCErrors_methods[4], controller, inp, done) -StreamRequestResponse(stub::GRPCErrorsBlockingStub, controller::ProtoRpcController, inp::Channel{Data}) = call_method(stub.impl, _GRPCErrors_methods[4], controller, inp) - -export Data, GRPCErrors, GRPCErrorsStub, GRPCErrorsBlockingStub, SimpleRPC, StreamResponse, StreamRequest, StreamRequestResponse diff --git a/test/RouteguideClients/RouteguideClients.jl b/test/RouteguideClients/RouteguideClients.jl deleted file mode 100644 index da4afcc..0000000 --- a/test/RouteguideClients/RouteguideClients.jl +++ /dev/null @@ -1,84 +0,0 @@ -module RouteguideClients -using gRPCClient - -include("routeguide.jl") -using .routeguide - -import Base: show - -# begin service: routeguide.RouteGuide - -export RouteGuideBlockingClient, RouteGuideClient - -struct RouteGuideBlockingClient - controller::gRPCController - channel::gRPCChannel - stub::RouteGuideBlockingStub - - function RouteGuideBlockingClient(api_base_url::String; kwargs...) - controller = gRPCController(; kwargs...) - channel = gRPCChannel(api_base_url) - stub = RouteGuideBlockingStub(channel) - new(controller, channel, stub) - end -end - -struct RouteGuideClient - controller::gRPCController - channel::gRPCChannel - stub::RouteGuideStub - - function RouteGuideClient(api_base_url::String; kwargs...) - controller = gRPCController(; kwargs...) - channel = gRPCChannel(api_base_url) - stub = RouteGuideStub(channel) - new(controller, channel, stub) - end -end - -show(io::IO, client::RouteGuideBlockingClient) = print(io, "RouteGuideBlockingClient(", client.channel.baseurl, ")") -show(io::IO, client::RouteGuideClient) = print(io, "RouteGuideClient(", client.channel.baseurl, ")") - -import .routeguide: GetFeature -""" - GetFeature - -- input: routeguide.Point -- output: routeguide.Feature -""" -GetFeature(client::RouteGuideBlockingClient, inp::routeguide.Point) = GetFeature(client.stub, client.controller, inp) -GetFeature(client::RouteGuideClient, inp::routeguide.Point, done::Function) = GetFeature(client.stub, client.controller, inp, done) - -import .routeguide: ListFeatures -""" - ListFeatures - -- input: routeguide.Rectangle -- output: Channel{routeguide.Feature} -""" -ListFeatures(client::RouteGuideBlockingClient, inp::routeguide.Rectangle) = ListFeatures(client.stub, client.controller, inp) -ListFeatures(client::RouteGuideClient, inp::routeguide.Rectangle, done::Function) = ListFeatures(client.stub, client.controller, inp, done) - -import .routeguide: RecordRoute -""" - RecordRoute - -- input: Channel{routeguide.Point} -- output: routeguide.RouteSummary -""" -RecordRoute(client::RouteGuideBlockingClient, inp::Channel{routeguide.Point}) = RecordRoute(client.stub, client.controller, inp) -RecordRoute(client::RouteGuideClient, inp::Channel{routeguide.Point}, done::Function) = RecordRoute(client.stub, client.controller, inp, done) - -import .routeguide: RouteChat -""" - RouteChat - -- input: Channel{routeguide.RouteNote} -- output: Channel{routeguide.RouteNote} -""" -RouteChat(client::RouteGuideBlockingClient, inp::Channel{routeguide.RouteNote}) = RouteChat(client.stub, client.controller, inp) -RouteChat(client::RouteGuideClient, inp::Channel{routeguide.RouteNote}, done::Function) = RouteChat(client.stub, client.controller, inp, done) - -# end service: routeguide.RouteGuide - -end # module RouteguideClients diff --git a/test/RouteguideClients/route_guide.proto b/test/RouteguideClients/route_guide.proto deleted file mode 100644 index 966c434..0000000 --- a/test/RouteguideClients/route_guide.proto +++ /dev/null @@ -1,111 +0,0 @@ -// Copyright 2015 gRPC authors. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -syntax = "proto3"; - -option go_package = "google.golang.org/grpc/examples/route_guide/routeguide"; -option java_multiple_files = true; -option java_package = "io.grpc.examples.routeguide"; -option java_outer_classname = "RouteGuideProto"; - -package routeguide; - -// Interface exported by the server. -service 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. - rpc GetFeature(Point) returns (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. - rpc ListFeatures(Rectangle) returns (stream Feature) {} - - // A client-to-server streaming RPC. - // - // 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. - // - // Accepts a stream of RouteNotes sent while a route is being traversed, - // while receiving other RouteNotes (e.g. from other users). - rpc RouteChat(stream RouteNote) returns (stream RouteNote) {} -} - -// Points are represented as latitude-longitude pairs in the E7 representation -// (degrees multiplied by 10**7 and rounded to the nearest integer). -// Latitudes should be in the range +/- 90 degrees and longitude should be in -// the range +/- 180 degrees (inclusive). -message Point { - int32 latitude = 1; - int32 longitude = 2; -} - -// A latitude-longitude rectangle, represented as two diagonally opposite -// points "lo" and "hi". -message Rectangle { - // One corner of the rectangle. - Point lo = 1; - - // The other corner of the rectangle. - Point hi = 2; -} - -// A feature names something at a given point. -// -// If a feature could not be named, the name is empty. -message Feature { - // The name of the feature. - string name = 1; - - // The point where the feature is detected. - Point location = 2; -} - -// A RouteNote is a message sent while at a given point. -message RouteNote { - // The location from which the message is sent. - Point location = 1; - - // The message to be sent. - string message = 2; -} - -// A RouteSummary is received in response to a RecordRoute rpc. -// -// It contains the number of individual points received, the number of -// detected features, and the total distance covered as the cumulative sum of -// the distance between each point. -message RouteSummary { - // The number of points received. - int32 point_count = 1; - - // The number of known features passed while traversing the route. - int32 feature_count = 2; - - // The distance covered in metres. - int32 distance = 3; - - // The duration of the traversal in seconds. - int32 elapsed_time = 4; -} diff --git a/test/RouteguideClients/route_guide_pb.jl b/test/RouteguideClients/route_guide_pb.jl deleted file mode 100644 index 3e91669..0000000 --- a/test/RouteguideClients/route_guide_pb.jl +++ /dev/null @@ -1,247 +0,0 @@ -# syntax: proto3 -using ProtoBuf -import ProtoBuf.meta - -mutable struct Point <: ProtoType - __protobuf_jl_internal_meta::ProtoMeta - __protobuf_jl_internal_values::Dict{Symbol,Any} - __protobuf_jl_internal_defaultset::Set{Symbol} - - function Point(; kwargs...) - obj = new(meta(Point), Dict{Symbol,Any}(), Set{Symbol}()) - values = obj.__protobuf_jl_internal_values - symdict = obj.__protobuf_jl_internal_meta.symdict - for nv in kwargs - fldname, fldval = nv - fldtype = symdict[fldname].jtyp - (fldname in keys(symdict)) || error(string(typeof(obj), " has no field with name ", fldname)) - if fldval !== nothing - values[fldname] = isa(fldval, fldtype) ? fldval : convert(fldtype, fldval) - end - end - obj - end -end # mutable struct Point -const __meta_Point = Ref{ProtoMeta}() -function meta(::Type{Point}) - ProtoBuf.metalock() do - if !isassigned(__meta_Point) - __meta_Point[] = target = ProtoMeta(Point) - allflds = Pair{Symbol,Union{Type,String}}[:latitude => Int32, :longitude => Int32] - meta(target, Point, allflds, ProtoBuf.DEF_REQ, ProtoBuf.DEF_FNUM, ProtoBuf.DEF_VAL, ProtoBuf.DEF_PACK, ProtoBuf.DEF_WTYPES, ProtoBuf.DEF_ONEOFS, ProtoBuf.DEF_ONEOF_NAMES) - end - __meta_Point[] - end -end -function Base.getproperty(obj::Point, name::Symbol) - if name === :latitude - return (obj.__protobuf_jl_internal_values[name])::Int32 - elseif name === :longitude - return (obj.__protobuf_jl_internal_values[name])::Int32 - else - getfield(obj, name) - end -end - -mutable struct Rectangle <: ProtoType - __protobuf_jl_internal_meta::ProtoMeta - __protobuf_jl_internal_values::Dict{Symbol,Any} - __protobuf_jl_internal_defaultset::Set{Symbol} - - function Rectangle(; kwargs...) - obj = new(meta(Rectangle), Dict{Symbol,Any}(), Set{Symbol}()) - values = obj.__protobuf_jl_internal_values - symdict = obj.__protobuf_jl_internal_meta.symdict - for nv in kwargs - fldname, fldval = nv - fldtype = symdict[fldname].jtyp - (fldname in keys(symdict)) || error(string(typeof(obj), " has no field with name ", fldname)) - if fldval !== nothing - values[fldname] = isa(fldval, fldtype) ? fldval : convert(fldtype, fldval) - end - end - obj - end -end # mutable struct Rectangle -const __meta_Rectangle = Ref{ProtoMeta}() -function meta(::Type{Rectangle}) - ProtoBuf.metalock() do - if !isassigned(__meta_Rectangle) - __meta_Rectangle[] = target = ProtoMeta(Rectangle) - allflds = Pair{Symbol,Union{Type,String}}[:lo => Point, :hi => Point] - meta(target, Rectangle, allflds, ProtoBuf.DEF_REQ, ProtoBuf.DEF_FNUM, ProtoBuf.DEF_VAL, ProtoBuf.DEF_PACK, ProtoBuf.DEF_WTYPES, ProtoBuf.DEF_ONEOFS, ProtoBuf.DEF_ONEOF_NAMES) - end - __meta_Rectangle[] - end -end -function Base.getproperty(obj::Rectangle, name::Symbol) - if name === :lo - return (obj.__protobuf_jl_internal_values[name])::Point - elseif name === :hi - return (obj.__protobuf_jl_internal_values[name])::Point - else - getfield(obj, name) - end -end - -mutable struct Feature <: ProtoType - __protobuf_jl_internal_meta::ProtoMeta - __protobuf_jl_internal_values::Dict{Symbol,Any} - __protobuf_jl_internal_defaultset::Set{Symbol} - - function Feature(; kwargs...) - obj = new(meta(Feature), Dict{Symbol,Any}(), Set{Symbol}()) - values = obj.__protobuf_jl_internal_values - symdict = obj.__protobuf_jl_internal_meta.symdict - for nv in kwargs - fldname, fldval = nv - fldtype = symdict[fldname].jtyp - (fldname in keys(symdict)) || error(string(typeof(obj), " has no field with name ", fldname)) - if fldval !== nothing - values[fldname] = isa(fldval, fldtype) ? fldval : convert(fldtype, fldval) - end - end - obj - end -end # mutable struct Feature -const __meta_Feature = Ref{ProtoMeta}() -function meta(::Type{Feature}) - ProtoBuf.metalock() do - if !isassigned(__meta_Feature) - __meta_Feature[] = target = ProtoMeta(Feature) - allflds = Pair{Symbol,Union{Type,String}}[:name => AbstractString, :location => Point] - meta(target, Feature, allflds, ProtoBuf.DEF_REQ, ProtoBuf.DEF_FNUM, ProtoBuf.DEF_VAL, ProtoBuf.DEF_PACK, ProtoBuf.DEF_WTYPES, ProtoBuf.DEF_ONEOFS, ProtoBuf.DEF_ONEOF_NAMES) - end - __meta_Feature[] - end -end -function Base.getproperty(obj::Feature, name::Symbol) - if name === :name - return (obj.__protobuf_jl_internal_values[name])::AbstractString - elseif name === :location - return (obj.__protobuf_jl_internal_values[name])::Point - else - getfield(obj, name) - end -end - -mutable struct RouteNote <: ProtoType - __protobuf_jl_internal_meta::ProtoMeta - __protobuf_jl_internal_values::Dict{Symbol,Any} - __protobuf_jl_internal_defaultset::Set{Symbol} - - function RouteNote(; kwargs...) - obj = new(meta(RouteNote), Dict{Symbol,Any}(), Set{Symbol}()) - values = obj.__protobuf_jl_internal_values - symdict = obj.__protobuf_jl_internal_meta.symdict - for nv in kwargs - fldname, fldval = nv - fldtype = symdict[fldname].jtyp - (fldname in keys(symdict)) || error(string(typeof(obj), " has no field with name ", fldname)) - if fldval !== nothing - values[fldname] = isa(fldval, fldtype) ? fldval : convert(fldtype, fldval) - end - end - obj - end -end # mutable struct RouteNote -const __meta_RouteNote = Ref{ProtoMeta}() -function meta(::Type{RouteNote}) - ProtoBuf.metalock() do - if !isassigned(__meta_RouteNote) - __meta_RouteNote[] = target = ProtoMeta(RouteNote) - allflds = Pair{Symbol,Union{Type,String}}[:location => Point, :message => AbstractString] - meta(target, RouteNote, allflds, ProtoBuf.DEF_REQ, ProtoBuf.DEF_FNUM, ProtoBuf.DEF_VAL, ProtoBuf.DEF_PACK, ProtoBuf.DEF_WTYPES, ProtoBuf.DEF_ONEOFS, ProtoBuf.DEF_ONEOF_NAMES) - end - __meta_RouteNote[] - end -end -function Base.getproperty(obj::RouteNote, name::Symbol) - if name === :location - return (obj.__protobuf_jl_internal_values[name])::Point - elseif name === :message - return (obj.__protobuf_jl_internal_values[name])::AbstractString - else - getfield(obj, name) - end -end - -mutable struct RouteSummary <: ProtoType - __protobuf_jl_internal_meta::ProtoMeta - __protobuf_jl_internal_values::Dict{Symbol,Any} - __protobuf_jl_internal_defaultset::Set{Symbol} - - function RouteSummary(; kwargs...) - obj = new(meta(RouteSummary), Dict{Symbol,Any}(), Set{Symbol}()) - values = obj.__protobuf_jl_internal_values - symdict = obj.__protobuf_jl_internal_meta.symdict - for nv in kwargs - fldname, fldval = nv - fldtype = symdict[fldname].jtyp - (fldname in keys(symdict)) || error(string(typeof(obj), " has no field with name ", fldname)) - if fldval !== nothing - values[fldname] = isa(fldval, fldtype) ? fldval : convert(fldtype, fldval) - end - end - obj - end -end # mutable struct RouteSummary -const __meta_RouteSummary = Ref{ProtoMeta}() -function meta(::Type{RouteSummary}) - ProtoBuf.metalock() do - if !isassigned(__meta_RouteSummary) - __meta_RouteSummary[] = target = ProtoMeta(RouteSummary) - allflds = Pair{Symbol,Union{Type,String}}[:point_count => Int32, :feature_count => Int32, :distance => Int32, :elapsed_time => Int32] - meta(target, RouteSummary, allflds, ProtoBuf.DEF_REQ, ProtoBuf.DEF_FNUM, ProtoBuf.DEF_VAL, ProtoBuf.DEF_PACK, ProtoBuf.DEF_WTYPES, ProtoBuf.DEF_ONEOFS, ProtoBuf.DEF_ONEOF_NAMES) - end - __meta_RouteSummary[] - end -end -function Base.getproperty(obj::RouteSummary, name::Symbol) - if name === :point_count - return (obj.__protobuf_jl_internal_values[name])::Int32 - elseif name === :feature_count - return (obj.__protobuf_jl_internal_values[name])::Int32 - elseif name === :distance - return (obj.__protobuf_jl_internal_values[name])::Int32 - elseif name === :elapsed_time - return (obj.__protobuf_jl_internal_values[name])::Int32 - else - getfield(obj, name) - end -end - -# service methods for RouteGuide -const _RouteGuide_methods = MethodDescriptor[ - MethodDescriptor("GetFeature", 1, Point, Feature), - MethodDescriptor("ListFeatures", 2, Rectangle, Channel{Feature}), - MethodDescriptor("RecordRoute", 3, Channel{Point}, RouteSummary), - MethodDescriptor("RouteChat", 4, Channel{RouteNote}, Channel{RouteNote}) - ] # const _RouteGuide_methods -const _RouteGuide_desc = ServiceDescriptor("routeguide.RouteGuide", 1, _RouteGuide_methods) - -RouteGuide(impl::Module) = ProtoService(_RouteGuide_desc, impl) - -mutable struct RouteGuideStub <: AbstractProtoServiceStub{false} - impl::ProtoServiceStub - RouteGuideStub(channel::ProtoRpcChannel) = new(ProtoServiceStub(_RouteGuide_desc, channel)) -end # mutable struct RouteGuideStub - -mutable struct RouteGuideBlockingStub <: AbstractProtoServiceStub{true} - impl::ProtoServiceBlockingStub - RouteGuideBlockingStub(channel::ProtoRpcChannel) = new(ProtoServiceBlockingStub(_RouteGuide_desc, channel)) -end # mutable struct RouteGuideBlockingStub - -GetFeature(stub::RouteGuideStub, controller::ProtoRpcController, inp::Point, done::Function) = call_method(stub.impl, _RouteGuide_methods[1], controller, inp, done) -GetFeature(stub::RouteGuideBlockingStub, controller::ProtoRpcController, inp::Point) = call_method(stub.impl, _RouteGuide_methods[1], controller, inp) - -ListFeatures(stub::RouteGuideStub, controller::ProtoRpcController, inp::Rectangle, done::Function) = call_method(stub.impl, _RouteGuide_methods[2], controller, inp, done) -ListFeatures(stub::RouteGuideBlockingStub, controller::ProtoRpcController, inp::Rectangle) = call_method(stub.impl, _RouteGuide_methods[2], controller, inp) - -RecordRoute(stub::RouteGuideStub, controller::ProtoRpcController, inp::Channel{Point}, done::Function) = call_method(stub.impl, _RouteGuide_methods[3], controller, inp, done) -RecordRoute(stub::RouteGuideBlockingStub, controller::ProtoRpcController, inp::Channel{Point}) = call_method(stub.impl, _RouteGuide_methods[3], controller, inp) - -RouteChat(stub::RouteGuideStub, controller::ProtoRpcController, inp::Channel{RouteNote}, done::Function) = call_method(stub.impl, _RouteGuide_methods[4], controller, inp, done) -RouteChat(stub::RouteGuideBlockingStub, controller::ProtoRpcController, inp::Channel{RouteNote}) = call_method(stub.impl, _RouteGuide_methods[4], controller, inp) - -export Point, Rectangle, Feature, RouteNote, RouteSummary, RouteGuide, RouteGuideStub, RouteGuideBlockingStub, GetFeature, ListFeatures, RecordRoute, RouteChat diff --git a/test/RouteguideClients/routeguide.jl b/test/RouteguideClients/routeguide.jl deleted file mode 100644 index 6dbe92d..0000000 --- a/test/RouteguideClients/routeguide.jl +++ /dev/null @@ -1,4 +0,0 @@ -module routeguide - const _ProtoBuf_Top_ = @static isdefined(parentmodule(@__MODULE__), :_ProtoBuf_Top_) ? (parentmodule(@__MODULE__))._ProtoBuf_Top_ : parentmodule(@__MODULE__) - include("route_guide_pb.jl") -end diff --git a/test/buildserver.sh b/test/buildserver.sh deleted file mode 100755 index 586daf1..0000000 --- a/test/buildserver.sh +++ /dev/null @@ -1,65 +0,0 @@ -#!/bin/bash -set -e - -BASEDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" -export PATH="$PATH:$(go env GOPATH)/bin" -mkdir -p ${BASEDIR}/testservers - -# build routeguide server -cd ${BASEDIR} -if [ ! -d "grpc-go" ] -then - git clone -b v1.35.0 https://github.com/grpc/grpc-go -fi - -cd grpc-go/examples/route_guide -protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative routeguide/route_guide.proto -sed 's/localhost/0.0.0.0/g' server/server.go > server/server.go.new -rm server/server.go -mv server/server.go.new server/server.go - -export GOOS=linux -export GOARCH=amd64 -echo "building routeguide_${GOOS}_${GOARCH}..." -go build -o routeguide_${GOOS}_${GOARCH} -i server/server.go -export GOARCH=386 -echo "building routeguide_${GOOS}_${GOARCH}..." -go build -o routeguide_${GOOS}_${GOARCH} -i server/server.go -export GOOS=windows -export GOARCH=amd64 -echo "building routeguide_${GOOS}_${GOARCH}..." -go build -o routeguide_${GOOS}_${GOARCH}.exe -i server/server.go -export GOARCH=386 -echo "building routeguide_${GOOS}_${GOARCH}..." -go build -o routeguide_${GOOS}_${GOARCH}.exe -i server/server.go -export GOOS=darwin -export GOARCH=amd64 -echo "building routeguide_${GOOS}_${GOARCH}..." -go build -o routeguide_${GOOS}_${GOARCH} -i server/server.go - -cp routeguide_* ${BASEDIR}/testservers/ - -# build grpcerrors server -cd ${BASEDIR} -cd error_test_server - -export GOOS=linux -export GOARCH=amd64 -echo "building grpcerrors_${GOOS}_${GOARCH}..." -go build -o grpcerrors_${GOOS}_${GOARCH} -i server.go -export GOARCH=386 -echo "building grpcerrors_${GOOS}_${GOARCH}..." -go build -o grpcerrors_${GOOS}_${GOARCH} -i server.go -export GOOS=windows -export GOARCH=amd64 -echo "building grpcerrors_${GOOS}_${GOARCH}..." -go build -o grpcerrors_${GOOS}_${GOARCH}.exe -i server.go -export GOARCH=386 -echo "building grpcerrors_${GOOS}_${GOARCH}..." -go build -o grpcerrors_${GOOS}_${GOARCH}.exe -i server.go -export GOOS=darwin -export GOARCH=amd64 -echo "building grpcerrors_${GOOS}_${GOARCH}..." -go build -o grpcerrors_${GOOS}_${GOARCH} -i server.go - -cp grpcerrors_* ${BASEDIR}/testservers/ diff --git a/test/error_test_server/.gitignore b/test/error_test_server/.gitignore deleted file mode 100644 index e9fe830..0000000 --- a/test/error_test_server/.gitignore +++ /dev/null @@ -1 +0,0 @@ -grpcerrors_* diff --git a/test/error_test_server/client.go b/test/error_test_server/client.go deleted file mode 100644 index c376f1b..0000000 --- a/test/error_test_server/client.go +++ /dev/null @@ -1,46 +0,0 @@ -package main - -import ( - "context" - "flag" - "log" - "time" - - "google.golang.org/grpc" - pb "juliacomputing.com/errortest/grpcerrors" -) - -var ( - tls = flag.Bool("tls", false, "Connection uses TLS if true, else plain TCP") - caFile = flag.String("ca_file", "", "The file containing the CA root cert file") - serverAddr = flag.String("server_addr", "localhost:10000", "The server address in the format of host:port") - serverHostOverride = flag.String("server_host_override", "x.test.youtube.com", "The server name used to verify the hostname returned by the TLS handshake") -) - -func simpleRPC(client pb.GRPCErrorsClient, data *pb.Data) { - log.Printf("Calling simpleRPC for data (%d, %d)", data.Mode, data.Param) - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) - defer cancel() - respdata, err := client.SimpleRPC(ctx, data) - if err != nil { - log.Fatalf("%v.SimpleRPC(_) = _, %v: ", client, err) - } - log.Println(respdata) -} - -func main() { - flag.Parse() - var opts []grpc.DialOption - opts = append(opts, grpc.WithInsecure()) - opts = append(opts, grpc.WithBlock()) - conn, err := grpc.Dial(*serverAddr, opts...) - if err != nil { - log.Fatalf("fail to dial: %v", err) - } - defer conn.Close() - client := pb.NewGRPCErrorsClient(conn) - - // simpel RPC - simpleRPC(client, &pb.Data{Mode: 1, Param: 0}) -} - diff --git a/test/error_test_server/go.mod b/test/error_test_server/go.mod deleted file mode 100644 index 83c3ebf..0000000 --- a/test/error_test_server/go.mod +++ /dev/null @@ -1,12 +0,0 @@ -module juliacomputing.com/errortest - -go 1.11 - -require ( - github.com/golang/protobuf v1.5.2 - golang.org/x/net v0.0.0-20210423184538-5f58ad60dda6 // indirect - golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7 // indirect - google.golang.org/genproto v0.0.0-20210423144448-3a41ef94ed2b // indirect - google.golang.org/grpc v1.37.0 - google.golang.org/protobuf v1.26.0 -) diff --git a/test/error_test_server/go.sum b/test/error_test_server/go.sum deleted file mode 100644 index 13b45f4..0000000 --- a/test/error_test_server/go.sum +++ /dev/null @@ -1,121 +0,0 @@ -cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= -github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= -github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= -github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= -github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= -github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= -github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= -github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= -github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= -github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= -github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= -github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= -golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= -golang.org/x/net v0.0.0-20210423184538-5f58ad60dda6 h1:0PC75Fz/kyMGhL0e1QnypqK2kQMqKt9csD1GnMJR+Zk= -golang.org/x/net v0.0.0-20210423184538-5f58ad60dda6/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= -golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7 h1:iGu644GcxtEcrInvDsQRCwJjtCIOlT2V7IRt6ah2Whw= -golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= -golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto v0.0.0-20210423144448-3a41ef94ed2b h1:Rt15zyw7G2yfLqmsjEa1xICjWEw+topkn7vEAR6bVPk= -google.golang.org/genproto v0.0.0-20210423144448-3a41ef94ed2b/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A= -google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= -google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.37.0 h1:uSZWeQJX5j11bIQ4AJoj+McDBo29cY1MCoC1wO3ts+c= -google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= -google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= -google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= -google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= -google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= -google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= -google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= -google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.26.0 h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk= -google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/test/error_test_server/grpcerrors/grpcerrors.pb.go b/test/error_test_server/grpcerrors/grpcerrors.pb.go deleted file mode 100644 index 4f51439..0000000 --- a/test/error_test_server/grpcerrors/grpcerrors.pb.go +++ /dev/null @@ -1,183 +0,0 @@ -// Code generated by protoc-gen-go. DO NOT EDIT. -// versions: -// protoc-gen-go v1.26.0 -// protoc v3.6.1 -// source: grpcerrors.proto - -package grpcerrors - -import ( - protoreflect "google.golang.org/protobuf/reflect/protoreflect" - protoimpl "google.golang.org/protobuf/runtime/protoimpl" - reflect "reflect" - sync "sync" -) - -const ( - // Verify that this generated code is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) - // Verify that runtime/protoimpl is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) -) - -// Request parameter, dictates how the simulation should behave. -// mode can take values: -// 1: throw an error after seconds provided in `param` -// 2: no error, just wait until seconds provided in `param`, respond with SimulationParams -// -// when sent in a stream as input, the server would consider only the first one in the -// stream to determine the course of action -type Data struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Mode int32 `protobuf:"varint,1,opt,name=mode,proto3" json:"mode,omitempty"` - Param int32 `protobuf:"varint,2,opt,name=param,proto3" json:"param,omitempty"` -} - -func (x *Data) Reset() { - *x = Data{} - if protoimpl.UnsafeEnabled { - mi := &file_grpcerrors_proto_msgTypes[0] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } -} - -func (x *Data) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*Data) ProtoMessage() {} - -func (x *Data) ProtoReflect() protoreflect.Message { - mi := &file_grpcerrors_proto_msgTypes[0] - if protoimpl.UnsafeEnabled && x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use Data.ProtoReflect.Descriptor instead. -func (*Data) Descriptor() ([]byte, []int) { - return file_grpcerrors_proto_rawDescGZIP(), []int{0} -} - -func (x *Data) GetMode() int32 { - if x != nil { - return x.Mode - } - return 0 -} - -func (x *Data) GetParam() int32 { - if x != nil { - return x.Param - } - return 0 -} - -var File_grpcerrors_proto protoreflect.FileDescriptor - -var file_grpcerrors_proto_rawDesc = []byte{ - 0x0a, 0x10, 0x67, 0x72, 0x70, 0x63, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x73, 0x2e, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x12, 0x0a, 0x67, 0x72, 0x70, 0x63, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x73, 0x22, 0x30, - 0x0a, 0x04, 0x44, 0x61, 0x74, 0x61, 0x12, 0x12, 0x0a, 0x04, 0x6d, 0x6f, 0x64, 0x65, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x05, 0x52, 0x04, 0x6d, 0x6f, 0x64, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x70, 0x61, - 0x72, 0x61, 0x6d, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x05, 0x70, 0x61, 0x72, 0x61, 0x6d, - 0x32, 0xf5, 0x01, 0x0a, 0x0a, 0x47, 0x52, 0x50, 0x43, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x73, 0x12, - 0x31, 0x0a, 0x09, 0x53, 0x69, 0x6d, 0x70, 0x6c, 0x65, 0x52, 0x50, 0x43, 0x12, 0x10, 0x2e, 0x67, - 0x72, 0x70, 0x63, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x73, 0x2e, 0x44, 0x61, 0x74, 0x61, 0x1a, 0x10, - 0x2e, 0x67, 0x72, 0x70, 0x63, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x73, 0x2e, 0x44, 0x61, 0x74, 0x61, - 0x22, 0x00, 0x12, 0x38, 0x0a, 0x0e, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x10, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x65, 0x72, 0x72, 0x6f, 0x72, - 0x73, 0x2e, 0x44, 0x61, 0x74, 0x61, 0x1a, 0x10, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x65, 0x72, 0x72, - 0x6f, 0x72, 0x73, 0x2e, 0x44, 0x61, 0x74, 0x61, 0x22, 0x00, 0x30, 0x01, 0x12, 0x37, 0x0a, 0x0d, - 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x10, 0x2e, - 0x67, 0x72, 0x70, 0x63, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x73, 0x2e, 0x44, 0x61, 0x74, 0x61, 0x1a, - 0x10, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x73, 0x2e, 0x44, 0x61, 0x74, - 0x61, 0x22, 0x00, 0x28, 0x01, 0x12, 0x41, 0x0a, 0x15, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x10, - 0x2e, 0x67, 0x72, 0x70, 0x63, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x73, 0x2e, 0x44, 0x61, 0x74, 0x61, - 0x1a, 0x10, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x73, 0x2e, 0x44, 0x61, - 0x74, 0x61, 0x22, 0x00, 0x28, 0x01, 0x30, 0x01, 0x42, 0x1f, 0x5a, 0x1d, 0x6a, 0x75, 0x6c, 0x69, - 0x61, 0x63, 0x6f, 0x6d, 0x70, 0x75, 0x74, 0x69, 0x6e, 0x67, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, - 0x72, 0x70, 0x63, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x33, -} - -var ( - file_grpcerrors_proto_rawDescOnce sync.Once - file_grpcerrors_proto_rawDescData = file_grpcerrors_proto_rawDesc -) - -func file_grpcerrors_proto_rawDescGZIP() []byte { - file_grpcerrors_proto_rawDescOnce.Do(func() { - file_grpcerrors_proto_rawDescData = protoimpl.X.CompressGZIP(file_grpcerrors_proto_rawDescData) - }) - return file_grpcerrors_proto_rawDescData -} - -var file_grpcerrors_proto_msgTypes = make([]protoimpl.MessageInfo, 1) -var file_grpcerrors_proto_goTypes = []interface{}{ - (*Data)(nil), // 0: grpcerrors.Data -} -var file_grpcerrors_proto_depIdxs = []int32{ - 0, // 0: grpcerrors.GRPCErrors.SimpleRPC:input_type -> grpcerrors.Data - 0, // 1: grpcerrors.GRPCErrors.StreamResponse:input_type -> grpcerrors.Data - 0, // 2: grpcerrors.GRPCErrors.StreamRequest:input_type -> grpcerrors.Data - 0, // 3: grpcerrors.GRPCErrors.StreamRequestResponse:input_type -> grpcerrors.Data - 0, // 4: grpcerrors.GRPCErrors.SimpleRPC:output_type -> grpcerrors.Data - 0, // 5: grpcerrors.GRPCErrors.StreamResponse:output_type -> grpcerrors.Data - 0, // 6: grpcerrors.GRPCErrors.StreamRequest:output_type -> grpcerrors.Data - 0, // 7: grpcerrors.GRPCErrors.StreamRequestResponse:output_type -> grpcerrors.Data - 4, // [4:8] is the sub-list for method output_type - 0, // [0:4] is the sub-list for method input_type - 0, // [0:0] is the sub-list for extension type_name - 0, // [0:0] is the sub-list for extension extendee - 0, // [0:0] is the sub-list for field type_name -} - -func init() { file_grpcerrors_proto_init() } -func file_grpcerrors_proto_init() { - if File_grpcerrors_proto != nil { - return - } - if !protoimpl.UnsafeEnabled { - file_grpcerrors_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Data); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - } - type x struct{} - out := protoimpl.TypeBuilder{ - File: protoimpl.DescBuilder{ - GoPackagePath: reflect.TypeOf(x{}).PkgPath(), - RawDescriptor: file_grpcerrors_proto_rawDesc, - NumEnums: 0, - NumMessages: 1, - NumExtensions: 0, - NumServices: 1, - }, - GoTypes: file_grpcerrors_proto_goTypes, - DependencyIndexes: file_grpcerrors_proto_depIdxs, - MessageInfos: file_grpcerrors_proto_msgTypes, - }.Build() - File_grpcerrors_proto = out.File - file_grpcerrors_proto_rawDesc = nil - file_grpcerrors_proto_goTypes = nil - file_grpcerrors_proto_depIdxs = nil -} diff --git a/test/error_test_server/grpcerrors/grpcerrors.proto b/test/error_test_server/grpcerrors/grpcerrors.proto deleted file mode 100644 index d53aaa9..0000000 --- a/test/error_test_server/grpcerrors/grpcerrors.proto +++ /dev/null @@ -1,32 +0,0 @@ -syntax = "proto3"; - -option go_package = "juliacomputing.com/grpcerrors"; - -package grpcerrors; - -// Interface exported by the server. -service GRPCErrors { - // simple RPC, takes a message and responds with a message - rpc SimpleRPC(Data) returns (Data) {} - - // streaming response, takes a message and responds with a stream - rpc StreamResponse(Data) returns (stream Data) {} - - // streaming request, takes streaming input and responds with a message - rpc StreamRequest(stream Data) returns (Data) {} - - // streaming request and response - rpc StreamRequestResponse(stream Data) returns (stream Data) {} -} - -// Request parameter, dictates how the simulation should behave. -// mode can take values: -// 1: throw an error after seconds provided in `param` -// 2: no error, just wait until seconds provided in `param`, respond with SimulationParams -// -// when sent in a stream as input, the server would consider only the first one in the -// stream to determine the course of action -message Data { - int32 mode = 1; - int32 param = 2; -} diff --git a/test/error_test_server/grpcerrors/grpcerrors_grpc.pb.go b/test/error_test_server/grpcerrors/grpcerrors_grpc.pb.go deleted file mode 100644 index 774bb7a..0000000 --- a/test/error_test_server/grpcerrors/grpcerrors_grpc.pb.go +++ /dev/null @@ -1,311 +0,0 @@ -// Code generated by protoc-gen-go-grpc. DO NOT EDIT. - -package grpcerrors - -import ( - context "context" - grpc "google.golang.org/grpc" - codes "google.golang.org/grpc/codes" - status "google.golang.org/grpc/status" -) - -// This is a compile-time assertion to ensure that this generated file -// is compatible with the grpc package it is being compiled against. -// Requires gRPC-Go v1.32.0 or later. -const _ = grpc.SupportPackageIsVersion7 - -// GRPCErrorsClient is the client API for GRPCErrors service. -// -// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. -type GRPCErrorsClient interface { - // simple RPC, takes a message and responds with a message - SimpleRPC(ctx context.Context, in *Data, opts ...grpc.CallOption) (*Data, error) - // streaming response, takes a message and responds with a stream - StreamResponse(ctx context.Context, in *Data, opts ...grpc.CallOption) (GRPCErrors_StreamResponseClient, error) - // streaming request, takes streaming input and responds with a message - StreamRequest(ctx context.Context, opts ...grpc.CallOption) (GRPCErrors_StreamRequestClient, error) - // streaming request and response - StreamRequestResponse(ctx context.Context, opts ...grpc.CallOption) (GRPCErrors_StreamRequestResponseClient, error) -} - -type gRPCErrorsClient struct { - cc grpc.ClientConnInterface -} - -func NewGRPCErrorsClient(cc grpc.ClientConnInterface) GRPCErrorsClient { - return &gRPCErrorsClient{cc} -} - -func (c *gRPCErrorsClient) SimpleRPC(ctx context.Context, in *Data, opts ...grpc.CallOption) (*Data, error) { - out := new(Data) - err := c.cc.Invoke(ctx, "/grpcerrors.GRPCErrors/SimpleRPC", in, out, opts...) - if err != nil { - return nil, err - } - return out, nil -} - -func (c *gRPCErrorsClient) StreamResponse(ctx context.Context, in *Data, opts ...grpc.CallOption) (GRPCErrors_StreamResponseClient, error) { - stream, err := c.cc.NewStream(ctx, &GRPCErrors_ServiceDesc.Streams[0], "/grpcerrors.GRPCErrors/StreamResponse", opts...) - if err != nil { - return nil, err - } - x := &gRPCErrorsStreamResponseClient{stream} - if err := x.ClientStream.SendMsg(in); err != nil { - return nil, err - } - if err := x.ClientStream.CloseSend(); err != nil { - return nil, err - } - return x, nil -} - -type GRPCErrors_StreamResponseClient interface { - Recv() (*Data, error) - grpc.ClientStream -} - -type gRPCErrorsStreamResponseClient struct { - grpc.ClientStream -} - -func (x *gRPCErrorsStreamResponseClient) Recv() (*Data, error) { - m := new(Data) - if err := x.ClientStream.RecvMsg(m); err != nil { - return nil, err - } - return m, nil -} - -func (c *gRPCErrorsClient) StreamRequest(ctx context.Context, opts ...grpc.CallOption) (GRPCErrors_StreamRequestClient, error) { - stream, err := c.cc.NewStream(ctx, &GRPCErrors_ServiceDesc.Streams[1], "/grpcerrors.GRPCErrors/StreamRequest", opts...) - if err != nil { - return nil, err - } - x := &gRPCErrorsStreamRequestClient{stream} - return x, nil -} - -type GRPCErrors_StreamRequestClient interface { - Send(*Data) error - CloseAndRecv() (*Data, error) - grpc.ClientStream -} - -type gRPCErrorsStreamRequestClient struct { - grpc.ClientStream -} - -func (x *gRPCErrorsStreamRequestClient) Send(m *Data) error { - return x.ClientStream.SendMsg(m) -} - -func (x *gRPCErrorsStreamRequestClient) CloseAndRecv() (*Data, error) { - if err := x.ClientStream.CloseSend(); err != nil { - return nil, err - } - m := new(Data) - if err := x.ClientStream.RecvMsg(m); err != nil { - return nil, err - } - return m, nil -} - -func (c *gRPCErrorsClient) StreamRequestResponse(ctx context.Context, opts ...grpc.CallOption) (GRPCErrors_StreamRequestResponseClient, error) { - stream, err := c.cc.NewStream(ctx, &GRPCErrors_ServiceDesc.Streams[2], "/grpcerrors.GRPCErrors/StreamRequestResponse", opts...) - if err != nil { - return nil, err - } - x := &gRPCErrorsStreamRequestResponseClient{stream} - return x, nil -} - -type GRPCErrors_StreamRequestResponseClient interface { - Send(*Data) error - Recv() (*Data, error) - grpc.ClientStream -} - -type gRPCErrorsStreamRequestResponseClient struct { - grpc.ClientStream -} - -func (x *gRPCErrorsStreamRequestResponseClient) Send(m *Data) error { - return x.ClientStream.SendMsg(m) -} - -func (x *gRPCErrorsStreamRequestResponseClient) Recv() (*Data, error) { - m := new(Data) - if err := x.ClientStream.RecvMsg(m); err != nil { - return nil, err - } - return m, nil -} - -// GRPCErrorsServer is the server API for GRPCErrors service. -// All implementations must embed UnimplementedGRPCErrorsServer -// for forward compatibility -type GRPCErrorsServer interface { - // simple RPC, takes a message and responds with a message - SimpleRPC(context.Context, *Data) (*Data, error) - // streaming response, takes a message and responds with a stream - StreamResponse(*Data, GRPCErrors_StreamResponseServer) error - // streaming request, takes streaming input and responds with a message - StreamRequest(GRPCErrors_StreamRequestServer) error - // streaming request and response - StreamRequestResponse(GRPCErrors_StreamRequestResponseServer) error - mustEmbedUnimplementedGRPCErrorsServer() -} - -// UnimplementedGRPCErrorsServer must be embedded to have forward compatible implementations. -type UnimplementedGRPCErrorsServer struct { -} - -func (UnimplementedGRPCErrorsServer) SimpleRPC(context.Context, *Data) (*Data, error) { - return nil, status.Errorf(codes.Unimplemented, "method SimpleRPC not implemented") -} -func (UnimplementedGRPCErrorsServer) StreamResponse(*Data, GRPCErrors_StreamResponseServer) error { - return status.Errorf(codes.Unimplemented, "method StreamResponse not implemented") -} -func (UnimplementedGRPCErrorsServer) StreamRequest(GRPCErrors_StreamRequestServer) error { - return status.Errorf(codes.Unimplemented, "method StreamRequest not implemented") -} -func (UnimplementedGRPCErrorsServer) StreamRequestResponse(GRPCErrors_StreamRequestResponseServer) error { - return status.Errorf(codes.Unimplemented, "method StreamRequestResponse not implemented") -} -func (UnimplementedGRPCErrorsServer) mustEmbedUnimplementedGRPCErrorsServer() {} - -// UnsafeGRPCErrorsServer may be embedded to opt out of forward compatibility for this service. -// Use of this interface is not recommended, as added methods to GRPCErrorsServer will -// result in compilation errors. -type UnsafeGRPCErrorsServer interface { - mustEmbedUnimplementedGRPCErrorsServer() -} - -func RegisterGRPCErrorsServer(s grpc.ServiceRegistrar, srv GRPCErrorsServer) { - s.RegisterService(&GRPCErrors_ServiceDesc, srv) -} - -func _GRPCErrors_SimpleRPC_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(Data) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(GRPCErrorsServer).SimpleRPC(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: "/grpcerrors.GRPCErrors/SimpleRPC", - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(GRPCErrorsServer).SimpleRPC(ctx, req.(*Data)) - } - return interceptor(ctx, in, info, handler) -} - -func _GRPCErrors_StreamResponse_Handler(srv interface{}, stream grpc.ServerStream) error { - m := new(Data) - if err := stream.RecvMsg(m); err != nil { - return err - } - return srv.(GRPCErrorsServer).StreamResponse(m, &gRPCErrorsStreamResponseServer{stream}) -} - -type GRPCErrors_StreamResponseServer interface { - Send(*Data) error - grpc.ServerStream -} - -type gRPCErrorsStreamResponseServer struct { - grpc.ServerStream -} - -func (x *gRPCErrorsStreamResponseServer) Send(m *Data) error { - return x.ServerStream.SendMsg(m) -} - -func _GRPCErrors_StreamRequest_Handler(srv interface{}, stream grpc.ServerStream) error { - return srv.(GRPCErrorsServer).StreamRequest(&gRPCErrorsStreamRequestServer{stream}) -} - -type GRPCErrors_StreamRequestServer interface { - SendAndClose(*Data) error - Recv() (*Data, error) - grpc.ServerStream -} - -type gRPCErrorsStreamRequestServer struct { - grpc.ServerStream -} - -func (x *gRPCErrorsStreamRequestServer) SendAndClose(m *Data) error { - return x.ServerStream.SendMsg(m) -} - -func (x *gRPCErrorsStreamRequestServer) Recv() (*Data, error) { - m := new(Data) - if err := x.ServerStream.RecvMsg(m); err != nil { - return nil, err - } - return m, nil -} - -func _GRPCErrors_StreamRequestResponse_Handler(srv interface{}, stream grpc.ServerStream) error { - return srv.(GRPCErrorsServer).StreamRequestResponse(&gRPCErrorsStreamRequestResponseServer{stream}) -} - -type GRPCErrors_StreamRequestResponseServer interface { - Send(*Data) error - Recv() (*Data, error) - grpc.ServerStream -} - -type gRPCErrorsStreamRequestResponseServer struct { - grpc.ServerStream -} - -func (x *gRPCErrorsStreamRequestResponseServer) Send(m *Data) error { - return x.ServerStream.SendMsg(m) -} - -func (x *gRPCErrorsStreamRequestResponseServer) Recv() (*Data, error) { - m := new(Data) - if err := x.ServerStream.RecvMsg(m); err != nil { - return nil, err - } - return m, nil -} - -// GRPCErrors_ServiceDesc is the grpc.ServiceDesc for GRPCErrors service. -// It's only intended for direct use with grpc.RegisterService, -// and not to be introspected or modified (even as a copy) -var GRPCErrors_ServiceDesc = grpc.ServiceDesc{ - ServiceName: "grpcerrors.GRPCErrors", - HandlerType: (*GRPCErrorsServer)(nil), - Methods: []grpc.MethodDesc{ - { - MethodName: "SimpleRPC", - Handler: _GRPCErrors_SimpleRPC_Handler, - }, - }, - Streams: []grpc.StreamDesc{ - { - StreamName: "StreamResponse", - Handler: _GRPCErrors_StreamResponse_Handler, - ServerStreams: true, - }, - { - StreamName: "StreamRequest", - Handler: _GRPCErrors_StreamRequest_Handler, - ClientStreams: true, - }, - { - StreamName: "StreamRequestResponse", - Handler: _GRPCErrors_StreamRequestResponse_Handler, - ServerStreams: true, - ClientStreams: true, - }, - }, - Metadata: "grpcerrors.proto", -} diff --git a/test/error_test_server/server.go b/test/error_test_server/server.go deleted file mode 100644 index b868e15..0000000 --- a/test/error_test_server/server.go +++ /dev/null @@ -1,125 +0,0 @@ -package main - -import ( - "context" - "errors" - "flag" - "fmt" - "io" - "log" - "net" - "time" - - "google.golang.org/grpc" - - "google.golang.org/grpc/codes" - "google.golang.org/grpc/credentials" - "google.golang.org/grpc/status" - - pb "juliacomputing.com/errortest/grpcerrors" -) - -var ( - tls = flag.Bool("tls", false, "Connection uses TLS if true, else plain TCP") - certFile = flag.String("cert_file", "", "The TLS cert file") - keyFile = flag.String("key_file", "", "The TLS key file") - port = flag.Int("port", 10000, "The server port") -) - -type gRPCErrorsServer struct { - pb.UnimplementedGRPCErrorsServer -} - -func (gRPCErrorsServer) SimpleRPC(ctx context.Context, data *pb.Data) (*pb.Data, error) { - if data.Mode == 1 { - time.Sleep(time.Duration(int64(data.Param)) * time.Second) - return data, errors.New("simulated error mode 1") - } else if data.Mode == 2 { - time.Sleep(time.Duration(int64(data.Param)) * time.Second) - return data, nil - } else { - return nil, status.Errorf(codes.Unimplemented, "mode not implemented") - } -} - -func (gRPCErrorsServer) StreamResponse(data *pb.Data, stream pb.GRPCErrors_StreamResponseServer) error { - if data.Mode == 1 { - time.Sleep(time.Duration(int64(data.Param)) * time.Second) - return errors.New("simulated error mode 1") - } else if data.Mode == 2 { - time.Sleep(time.Duration(int64(data.Param)) * time.Second) - if err := stream.Send(data); err != nil { - return err - } - return nil - } else { - return status.Errorf(codes.Unimplemented, "mode not implemented") - } -} - -func (gRPCErrorsServer) StreamRequest(stream pb.GRPCErrors_StreamRequestServer) error { - data, err := stream.Recv() - if err == io.EOF { - return nil - } - if err != nil { - return err - } - - if data.Mode == 1 { - time.Sleep(time.Duration(int64(data.Param)) * time.Second) - return errors.New("simulated error mode 1") - } else if data.Mode == 2 { - time.Sleep(time.Duration(int64(data.Param)) * time.Second) - return stream.SendAndClose(data) - } else { - return status.Errorf(codes.Unimplemented, "mode not implemented") - } -} - -func (gRPCErrorsServer) StreamRequestResponse(stream pb.GRPCErrors_StreamRequestResponseServer) error { - data, err := stream.Recv() - if err == io.EOF { - return nil - } - if err != nil { - return err - } - - if data.Mode == 1 { - time.Sleep(time.Duration(int64(data.Param)) * time.Second) - return errors.New("simulated error mode 1") - } else if data.Mode == 2 { - time.Sleep(time.Duration(int64(data.Param)) * time.Second) - if err := stream.Send(data); err != nil { - return err - } - return nil - } else { - return status.Errorf(codes.Unimplemented, "mode not implemented") - } -} - -func newServer() *gRPCErrorsServer { - s := &gRPCErrorsServer{} - return s -} - -func main() { - flag.Parse() - lis, err := net.Listen("tcp", fmt.Sprintf("0.0.0.0:%d", *port)) - if err != nil { - log.Fatalf("failed to listen: %v", err) - } - var opts []grpc.ServerOption - if *tls { - creds, err := credentials.NewServerTLSFromFile(*certFile, *keyFile) - if err != nil { - log.Fatalf("Failed to generate credentials %v", err) - } - opts = []grpc.ServerOption{grpc.Creds(creds)} - } - grpcServer := grpc.NewServer(opts...) - pb.RegisterGRPCErrorsServer(grpcServer, newServer()) - grpcServer.Serve(lis) -} diff --git a/test/gen/test/test.jl b/test/gen/test/test.jl new file mode 100644 index 0000000..4940c12 --- /dev/null +++ b/test/gen/test/test.jl @@ -0,0 +1,5 @@ +module test + +include("test_pb.jl") + +end # module test diff --git a/test/gen/test/test_pb.jl b/test/gen/test/test_pb.jl new file mode 100644 index 0000000..92f58af --- /dev/null +++ b/test/gen/test/test_pb.jl @@ -0,0 +1,148 @@ +# Autogenerated using ProtoBuf.jl v1.2.0 on 2025-10-20T07:36:28.709 +# original file: /home/cvance@medicalmetrics.local/PycharmProjects/gRPCClient2.jl/test/proto/test.proto (proto3 syntax) + +import ProtoBuf as PB +using ProtoBuf: OneOf +using ProtoBuf.EnumX: @enumx + +export TestResponse, TestRequest + + +struct TestResponse + data::Vector{UInt64} +end +PB.default_values(::Type{TestResponse}) = (;data = Vector{UInt64}()) +PB.field_numbers(::Type{TestResponse}) = (;data = 1) + +function PB.decode(d::PB.AbstractProtoDecoder, ::Type{<:TestResponse}) + data = PB.BufferedVector{UInt64}() + while !PB.message_done(d) + field_number, wire_type = PB.decode_tag(d) + if field_number == 1 + PB.decode!(d, wire_type, data) + else + Base.skip(d, wire_type) + end + end + return TestResponse(data[]) +end + +function PB.encode(e::PB.AbstractProtoEncoder, x::TestResponse) + initpos = position(e.io) + !isempty(x.data) && PB.encode(e, 1, x.data) + return position(e.io) - initpos +end +function PB._encoded_size(x::TestResponse) + encoded_size = 0 + !isempty(x.data) && (encoded_size += PB._encoded_size(x.data, 1)) + return encoded_size +end + +struct TestRequest + test_response_sz::UInt64 + data::Vector{UInt64} +end +PB.default_values(::Type{TestRequest}) = (;test_response_sz = zero(UInt64), data = Vector{UInt64}()) +PB.field_numbers(::Type{TestRequest}) = (;test_response_sz = 1, data = 2) + +function PB.decode(d::PB.AbstractProtoDecoder, ::Type{<:TestRequest}) + test_response_sz = zero(UInt64) + data = PB.BufferedVector{UInt64}() + while !PB.message_done(d) + field_number, wire_type = PB.decode_tag(d) + if field_number == 1 + test_response_sz = PB.decode(d, UInt64) + elseif field_number == 2 + PB.decode!(d, wire_type, data) + else + Base.skip(d, wire_type) + end + end + return TestRequest(test_response_sz, data[]) +end + +function PB.encode(e::PB.AbstractProtoEncoder, x::TestRequest) + initpos = position(e.io) + x.test_response_sz != zero(UInt64) && PB.encode(e, 1, x.test_response_sz) + !isempty(x.data) && PB.encode(e, 2, x.data) + return position(e.io) - initpos +end +function PB._encoded_size(x::TestRequest) + encoded_size = 0 + x.test_response_sz != zero(UInt64) && (encoded_size += PB._encoded_size(x.test_response_sz, 1)) + !isempty(x.data) && (encoded_size += PB._encoded_size(x.data, 2)) + return encoded_size +end + +TestService_TestRPC_Client( + host, port; + secure=false, + grpc=grpc_global_handle(), + deadline=10, + keepalive=60, + max_send_message_length = 4*1024*1024, + max_recieve_message_length = 4*1024*1024, +) = gRPCServiceClient{TestRequest, false, TestResponse, false}( + host, port, "/test.TestService/TestRPC"; + secure=secure, + grpc=grpc, + deadline=deadline, + keepalive=keepalive, + max_send_message_length=max_send_message_length, + max_recieve_message_length=max_recieve_message_length, +) + +TestService_TestServerStreamRPC_Client( + host, port; + secure=false, + grpc=grpc_global_handle(), + deadline=10, + keepalive=60, + max_send_message_length = 4*1024*1024, + max_recieve_message_length = 4*1024*1024, +) = gRPCServiceClient{TestRequest, false, TestResponse, true}( + host, port, "/test.TestService/TestServerStreamRPC"; + secure=secure, + grpc=grpc, + deadline=deadline, + keepalive=keepalive, + max_send_message_length=max_send_message_length, + max_recieve_message_length=max_recieve_message_length, +) + +TestService_TestClientStreamRPC_Client( + host, port; + secure=false, + grpc=grpc_global_handle(), + deadline=10, + keepalive=60, + max_send_message_length = 4*1024*1024, + max_recieve_message_length = 4*1024*1024, +) = gRPCServiceClient{TestRequest, true, TestResponse, false}( + host, port, "/test.TestService/TestClientStreamRPC"; + secure=secure, + grpc=grpc, + deadline=deadline, + keepalive=keepalive, + max_send_message_length=max_send_message_length, + max_recieve_message_length=max_recieve_message_length, +) + +TestService_TestBidirectionalStreamRPC_Client( + host, port; + secure=false, + grpc=grpc_global_handle(), + deadline=10, + keepalive=60, + max_send_message_length = 4*1024*1024, + max_recieve_message_length = 4*1024*1024, +) = gRPCServiceClient{TestRequest, true, TestResponse, true}( + host, port, "/test.TestService/TestBidirectionalStreamRPC"; + secure=secure, + grpc=grpc, + deadline=deadline, + keepalive=keepalive, + max_send_message_length=max_send_message_length, + max_recieve_message_length=max_recieve_message_length, +) + diff --git a/test/proto/test.proto b/test/proto/test.proto new file mode 100644 index 0000000..ef877c0 --- /dev/null +++ b/test/proto/test.proto @@ -0,0 +1,18 @@ +syntax = "proto3"; +package test; + +service TestService { + rpc TestRPC(TestRequest) returns (TestResponse) {} + rpc TestServerStreamRPC(TestRequest) returns (stream TestResponse) {} + rpc TestClientStreamRPC(stream TestRequest) returns (TestResponse) {} + rpc TestBidirectionalStreamRPC(stream TestRequest) returns (stream TestResponse) {} +} + +message TestRequest { + uint64 test_response_sz = 1; + repeated uint64 data = 2; +} + +message TestResponse { + repeated uint64 data = 1; +} diff --git a/test/protoc.sh b/test/protoc.sh new file mode 100755 index 0000000..dcf679e --- /dev/null +++ b/test/protoc.sh @@ -0,0 +1,16 @@ +#!/bin/bash + +pushd python +uv run -m grpc_tools.protoc -I ../proto --python_out=. --pyi_out=. --grpc_python_out=. ../proto/test.proto +popd + +mkdir -p gen + +julia <:|" + # For testing + response_data = np.arange(request.test_response_sz, dtype=np.uint64) + response_data[:] += 1 + else: + # For precompile + response_data = np.arange(1, dtype=np.uint64) + response_data[:] += 1 + + return test_pb2.TestResponse(data=response_data) + + def TestClientStreamRPC(self, request_iterator, context): + rs = 0 + for request in request_iterator: + rs += request.test_response_sz + + if not self.public: + # For testing + response_data = np.arange(rs, dtype=np.uint64) + response_data[:] += 1 + else: + # For precompile + response_data = np.arange(1, dtype=np.uint64) + response_data[:] += 1 + + return test_pb2.TestResponse(data=response_data) + + def TestServerStreamRPC(self, request, context): + + if not self.public: + # For testing + for i in range(1, request.test_response_sz + 1): + response_data = np.arange(i, dtype=np.uint64) + response_data[:] += 1 + yield test_pb2.TestResponse(data=response_data) + else: + # For precompile + response_data = np.arange(1, dtype=np.uint64) + response_data[:] += 1 + + yield test_pb2.TestResponse(data=response_data) + + + def TestBidirectionalStreamRPC(self, request_iterator, context): + for request in request_iterator: + if not self.public: + response_data = np.arange(request.test_response_sz, dtype=np.uint64) + response_data[:] += 1 + else: + response_data = np.arange(1, dtype=np.uint64) + response_data[:] += 1 + + yield test_pb2.TestResponse(data=response_data) + + +def serve(public: bool = False): + server = grpc.server(futures.ThreadPoolExecutor(max_workers=12)) + test_pb2_grpc.add_TestServiceServicer_to_server( + TestServiceServicer(public=public), server + ) + if public: + bind_address = "[::]:8001" + logging.info("Listening on %s in public mode" % bind_address) + logging.info("(len(response.data) will always be 1)") + server.add_insecure_port(bind_address) + else: + host = '127.0.0.1' if 'GRPC_TEST_SERVER_HOST' not in os.environ else os.environ['GRPC_TEST_SERVER_HOST'] + port = 8001 if 'GRPC_TEST_SERVER_PORT' not in os.environ else int(os.environ['GRPC_TEST_SERVER_PORT']) + bind_address = f"{host}:{port}" + logging.info("Listening on %s in test mode" % bind_address) + server.add_insecure_port(bind_address) + + server.start() + server.wait_for_termination() + + +if __name__ == "__main__": + logging.basicConfig(level=logging.DEBUG) + serve(public=len(sys.argv) > 1 and sys.argv[1] == 'public') \ No newline at end of file diff --git a/test/python/pyproject.toml b/test/python/pyproject.toml new file mode 100644 index 0000000..c84f515 --- /dev/null +++ b/test/python/pyproject.toml @@ -0,0 +1,13 @@ +[project] +name = "grpc-test-server" +version = "0.1.0" +description = "Add your description here" +readme = "README.md" +requires-python = ">=3.13" +dependencies = [ + "black>=25.9.0", + "grpcio>=1.75.1", + "grpcio-tools>=1.75.1", + "numpy>=2.3.3", + "plac>=1.4.5", +] diff --git a/test/python/test_pb2.py b/test/python/test_pb2.py new file mode 100644 index 0000000..33478d6 --- /dev/null +++ b/test/python/test_pb2.py @@ -0,0 +1,40 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# NO CHECKED-IN PROTOBUF GENCODE +# source: test.proto +# Protobuf Python Version: 6.31.1 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import runtime_version as _runtime_version +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +_runtime_version.ValidateProtobufRuntimeVersion( + _runtime_version.Domain.PUBLIC, + 6, + 31, + 1, + '', + 'test.proto' +) +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\ntest.proto\x12\x04test\"5\n\x0bTestRequest\x12\x18\n\x10test_response_sz\x18\x01 \x01(\x04\x12\x0c\n\x04\x64\x61ta\x18\x02 \x03(\x04\"\x1c\n\x0cTestResponse\x12\x0c\n\x04\x64\x61ta\x18\x01 \x03(\x04\x32\x90\x02\n\x0bTestService\x12\x32\n\x07TestRPC\x12\x11.test.TestRequest\x1a\x12.test.TestResponse\"\x00\x12@\n\x13TestServerStreamRPC\x12\x11.test.TestRequest\x1a\x12.test.TestResponse\"\x00\x30\x01\x12@\n\x13TestClientStreamRPC\x12\x11.test.TestRequest\x1a\x12.test.TestResponse\"\x00(\x01\x12I\n\x1aTestBidirectionalStreamRPC\x12\x11.test.TestRequest\x1a\x12.test.TestResponse\"\x00(\x01\x30\x01\x62\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'test_pb2', _globals) +if not _descriptor._USE_C_DESCRIPTORS: + DESCRIPTOR._loaded_options = None + _globals['_TESTREQUEST']._serialized_start=20 + _globals['_TESTREQUEST']._serialized_end=73 + _globals['_TESTRESPONSE']._serialized_start=75 + _globals['_TESTRESPONSE']._serialized_end=103 + _globals['_TESTSERVICE']._serialized_start=106 + _globals['_TESTSERVICE']._serialized_end=378 +# @@protoc_insertion_point(module_scope) diff --git a/test/python/test_pb2.pyi b/test/python/test_pb2.pyi new file mode 100644 index 0000000..5d293de --- /dev/null +++ b/test/python/test_pb2.pyi @@ -0,0 +1,21 @@ +from google.protobuf.internal import containers as _containers +from google.protobuf import descriptor as _descriptor +from google.protobuf import message as _message +from collections.abc import Iterable as _Iterable +from typing import ClassVar as _ClassVar, Optional as _Optional + +DESCRIPTOR: _descriptor.FileDescriptor + +class TestRequest(_message.Message): + __slots__ = ("test_response_sz", "data") + TEST_RESPONSE_SZ_FIELD_NUMBER: _ClassVar[int] + DATA_FIELD_NUMBER: _ClassVar[int] + test_response_sz: int + data: _containers.RepeatedScalarFieldContainer[int] + def __init__(self, test_response_sz: _Optional[int] = ..., data: _Optional[_Iterable[int]] = ...) -> None: ... + +class TestResponse(_message.Message): + __slots__ = ("data",) + DATA_FIELD_NUMBER: _ClassVar[int] + data: _containers.RepeatedScalarFieldContainer[int] + def __init__(self, data: _Optional[_Iterable[int]] = ...) -> None: ... diff --git a/test/python/test_pb2_grpc.py b/test/python/test_pb2_grpc.py new file mode 100644 index 0000000..02f3dc8 --- /dev/null +++ b/test/python/test_pb2_grpc.py @@ -0,0 +1,226 @@ +# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! +"""Client and server classes corresponding to protobuf-defined services.""" +import grpc +import warnings + +import test_pb2 as test__pb2 + +GRPC_GENERATED_VERSION = '1.75.1' +GRPC_VERSION = grpc.__version__ +_version_not_supported = False + +try: + from grpc._utilities import first_version_is_lower + _version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION) +except ImportError: + _version_not_supported = True + +if _version_not_supported: + raise RuntimeError( + f'The grpc package installed is at version {GRPC_VERSION},' + + f' but the generated code in test_pb2_grpc.py depends on' + + f' grpcio>={GRPC_GENERATED_VERSION}.' + + f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}' + + f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.' + ) + + +class TestServiceStub(object): + """Missing associated documentation comment in .proto file.""" + + def __init__(self, channel): + """Constructor. + + Args: + channel: A grpc.Channel. + """ + self.TestRPC = channel.unary_unary( + '/test.TestService/TestRPC', + request_serializer=test__pb2.TestRequest.SerializeToString, + response_deserializer=test__pb2.TestResponse.FromString, + _registered_method=True) + self.TestServerStreamRPC = channel.unary_stream( + '/test.TestService/TestServerStreamRPC', + request_serializer=test__pb2.TestRequest.SerializeToString, + response_deserializer=test__pb2.TestResponse.FromString, + _registered_method=True) + self.TestClientStreamRPC = channel.stream_unary( + '/test.TestService/TestClientStreamRPC', + request_serializer=test__pb2.TestRequest.SerializeToString, + response_deserializer=test__pb2.TestResponse.FromString, + _registered_method=True) + self.TestBidirectionalStreamRPC = channel.stream_stream( + '/test.TestService/TestBidirectionalStreamRPC', + request_serializer=test__pb2.TestRequest.SerializeToString, + response_deserializer=test__pb2.TestResponse.FromString, + _registered_method=True) + + +class TestServiceServicer(object): + """Missing associated documentation comment in .proto file.""" + + def TestRPC(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def TestServerStreamRPC(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def TestClientStreamRPC(self, request_iterator, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def TestBidirectionalStreamRPC(self, request_iterator, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + +def add_TestServiceServicer_to_server(servicer, server): + rpc_method_handlers = { + 'TestRPC': grpc.unary_unary_rpc_method_handler( + servicer.TestRPC, + request_deserializer=test__pb2.TestRequest.FromString, + response_serializer=test__pb2.TestResponse.SerializeToString, + ), + 'TestServerStreamRPC': grpc.unary_stream_rpc_method_handler( + servicer.TestServerStreamRPC, + request_deserializer=test__pb2.TestRequest.FromString, + response_serializer=test__pb2.TestResponse.SerializeToString, + ), + 'TestClientStreamRPC': grpc.stream_unary_rpc_method_handler( + servicer.TestClientStreamRPC, + request_deserializer=test__pb2.TestRequest.FromString, + response_serializer=test__pb2.TestResponse.SerializeToString, + ), + 'TestBidirectionalStreamRPC': grpc.stream_stream_rpc_method_handler( + servicer.TestBidirectionalStreamRPC, + request_deserializer=test__pb2.TestRequest.FromString, + response_serializer=test__pb2.TestResponse.SerializeToString, + ), + } + generic_handler = grpc.method_handlers_generic_handler( + 'test.TestService', rpc_method_handlers) + server.add_generic_rpc_handlers((generic_handler,)) + server.add_registered_method_handlers('test.TestService', rpc_method_handlers) + + + # This class is part of an EXPERIMENTAL API. +class TestService(object): + """Missing associated documentation comment in .proto file.""" + + @staticmethod + def TestRPC(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/test.TestService/TestRPC', + test__pb2.TestRequest.SerializeToString, + test__pb2.TestResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def TestServerStreamRPC(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_stream( + request, + target, + '/test.TestService/TestServerStreamRPC', + test__pb2.TestRequest.SerializeToString, + test__pb2.TestResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def TestClientStreamRPC(request_iterator, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.stream_unary( + request_iterator, + target, + '/test.TestService/TestClientStreamRPC', + test__pb2.TestRequest.SerializeToString, + test__pb2.TestResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def TestBidirectionalStreamRPC(request_iterator, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.stream_stream( + request_iterator, + target, + '/test.TestService/TestBidirectionalStreamRPC', + test__pb2.TestRequest.SerializeToString, + test__pb2.TestResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) diff --git a/test/python/uv.lock b/test/python/uv.lock new file mode 100644 index 0000000..7a18689 --- /dev/null +++ b/test/python/uv.lock @@ -0,0 +1,268 @@ +version = 1 +revision = 3 +requires-python = ">=3.13" + +[[package]] +name = "black" +version = "25.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "mypy-extensions" }, + { name = "packaging" }, + { name = "pathspec" }, + { name = "platformdirs" }, + { name = "pytokens" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4b/43/20b5c90612d7bdb2bdbcceeb53d588acca3bb8f0e4c5d5c751a2c8fdd55a/black-25.9.0.tar.gz", hash = "sha256:0474bca9a0dd1b51791fcc507a4e02078a1c63f6d4e4ae5544b9848c7adfb619", size = 648393, upload-time = "2025-09-19T00:27:37.758Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/48/99/3acfea65f5e79f45472c45f87ec13037b506522719cd9d4ac86484ff51ac/black-25.9.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0172a012f725b792c358d57fe7b6b6e8e67375dd157f64fa7a3097b3ed3e2175", size = 1742165, upload-time = "2025-09-19T00:34:10.402Z" }, + { url = "https://files.pythonhosted.org/packages/3a/18/799285282c8236a79f25d590f0222dbd6850e14b060dfaa3e720241fd772/black-25.9.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3bec74ee60f8dfef564b573a96b8930f7b6a538e846123d5ad77ba14a8d7a64f", size = 1581259, upload-time = "2025-09-19T00:32:49.685Z" }, + { url = "https://files.pythonhosted.org/packages/f1/ce/883ec4b6303acdeca93ee06b7622f1fa383c6b3765294824165d49b1a86b/black-25.9.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b756fc75871cb1bcac5499552d771822fd9db5a2bb8db2a7247936ca48f39831", size = 1655583, upload-time = "2025-09-19T00:30:44.505Z" }, + { url = "https://files.pythonhosted.org/packages/21/17/5c253aa80a0639ccc427a5c7144534b661505ae2b5a10b77ebe13fa25334/black-25.9.0-cp313-cp313-win_amd64.whl", hash = "sha256:846d58e3ce7879ec1ffe816bb9df6d006cd9590515ed5d17db14e17666b2b357", size = 1343428, upload-time = "2025-09-19T00:32:13.839Z" }, + { url = "https://files.pythonhosted.org/packages/1b/46/863c90dcd3f9d41b109b7f19032ae0db021f0b2a81482ba0a1e28c84de86/black-25.9.0-py3-none-any.whl", hash = "sha256:474b34c1342cdc157d307b56c4c65bce916480c4a8f6551fdc6bf9b486a7c4ae", size = 203363, upload-time = "2025-09-19T00:27:35.724Z" }, +] + +[[package]] +name = "click" +version = "8.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/46/61/de6cd827efad202d7057d93e0fed9294b96952e188f7384832791c7b2254/click-8.3.0.tar.gz", hash = "sha256:e7b8232224eba16f4ebe410c25ced9f7875cb5f3263ffc93cc3e8da705e229c4", size = 276943, upload-time = "2025-09-18T17:32:23.696Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/d3/9dcc0f5797f070ec8edf30fbadfb200e71d9db6b84d211e3b2085a7589a0/click-8.3.0-py3-none-any.whl", hash = "sha256:9b9f285302c6e3064f4330c05f05b81945b2a39544279343e6e7c5f27a9baddc", size = 107295, upload-time = "2025-09-18T17:32:22.42Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "grpc-test-server" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "black" }, + { name = "grpcio" }, + { name = "grpcio-tools" }, + { name = "numpy" }, + { name = "plac" }, +] + +[package.metadata] +requires-dist = [ + { name = "black", specifier = ">=25.9.0" }, + { name = "grpcio", specifier = ">=1.75.1" }, + { name = "grpcio-tools", specifier = ">=1.75.1" }, + { name = "numpy", specifier = ">=2.3.3" }, + { name = "plac", specifier = ">=1.4.5" }, +] + +[[package]] +name = "grpcio" +version = "1.75.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9d/f7/8963848164c7604efb3a3e6ee457fdb3a469653e19002bd24742473254f8/grpcio-1.75.1.tar.gz", hash = "sha256:3e81d89ece99b9ace23a6916880baca613c03a799925afb2857887efa8b1b3d2", size = 12731327, upload-time = "2025-09-26T09:03:36.887Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/74/bac4ab9f7722164afdf263ae31ba97b8174c667153510322a5eba4194c32/grpcio-1.75.1-cp313-cp313-linux_armv7l.whl", hash = "sha256:3bed22e750d91d53d9e31e0af35a7b0b51367e974e14a4ff229db5b207647884", size = 5672779, upload-time = "2025-09-26T09:02:19.11Z" }, + { url = "https://files.pythonhosted.org/packages/a6/52/d0483cfa667cddaa294e3ab88fd2c2a6e9dc1a1928c0e5911e2e54bd5b50/grpcio-1.75.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:5b8f381eadcd6ecaa143a21e9e80a26424c76a0a9b3d546febe6648f3a36a5ac", size = 11470623, upload-time = "2025-09-26T09:02:22.117Z" }, + { url = "https://files.pythonhosted.org/packages/cf/e4/d1954dce2972e32384db6a30273275e8c8ea5a44b80347f9055589333b3f/grpcio-1.75.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5bf4001d3293e3414d0cf99ff9b1139106e57c3a66dfff0c5f60b2a6286ec133", size = 6248838, upload-time = "2025-09-26T09:02:26.426Z" }, + { url = "https://files.pythonhosted.org/packages/06/43/073363bf63826ba8077c335d797a8d026f129dc0912b69c42feaf8f0cd26/grpcio-1.75.1-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:9f82ff474103e26351dacfe8d50214e7c9322960d8d07ba7fa1d05ff981c8b2d", size = 6922663, upload-time = "2025-09-26T09:02:28.724Z" }, + { url = "https://files.pythonhosted.org/packages/c2/6f/076ac0df6c359117676cacfa8a377e2abcecec6a6599a15a672d331f6680/grpcio-1.75.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0ee119f4f88d9f75414217823d21d75bfe0e6ed40135b0cbbfc6376bc9f7757d", size = 6436149, upload-time = "2025-09-26T09:02:30.971Z" }, + { url = "https://files.pythonhosted.org/packages/6b/27/1d08824f1d573fcb1fa35ede40d6020e68a04391709939e1c6f4193b445f/grpcio-1.75.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:664eecc3abe6d916fa6cf8dd6b778e62fb264a70f3430a3180995bf2da935446", size = 7067989, upload-time = "2025-09-26T09:02:33.233Z" }, + { url = "https://files.pythonhosted.org/packages/c6/98/98594cf97b8713feb06a8cb04eeef60b4757e3e2fb91aa0d9161da769843/grpcio-1.75.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:c32193fa08b2fbebf08fe08e84f8a0aad32d87c3ad42999c65e9449871b1c66e", size = 8010717, upload-time = "2025-09-26T09:02:36.011Z" }, + { url = "https://files.pythonhosted.org/packages/8c/7e/bb80b1bba03c12158f9254762cdf5cced4a9bc2e8ed51ed335915a5a06ef/grpcio-1.75.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5cebe13088b9254f6e615bcf1da9131d46cfa4e88039454aca9cb65f639bd3bc", size = 7463822, upload-time = "2025-09-26T09:02:38.26Z" }, + { url = "https://files.pythonhosted.org/packages/23/1c/1ea57fdc06927eb5640f6750c697f596f26183573069189eeaf6ef86ba2d/grpcio-1.75.1-cp313-cp313-win32.whl", hash = "sha256:4b4c678e7ed50f8ae8b8dbad15a865ee73ce12668b6aaf411bf3258b5bc3f970", size = 3938490, upload-time = "2025-09-26T09:02:40.268Z" }, + { url = "https://files.pythonhosted.org/packages/4b/24/fbb8ff1ccadfbf78ad2401c41aceaf02b0d782c084530d8871ddd69a2d49/grpcio-1.75.1-cp313-cp313-win_amd64.whl", hash = "sha256:5573f51e3f296a1bcf71e7a690c092845fb223072120f4bdb7a5b48e111def66", size = 4642538, upload-time = "2025-09-26T09:02:42.519Z" }, + { url = "https://files.pythonhosted.org/packages/f2/1b/9a0a5cecd24302b9fdbcd55d15ed6267e5f3d5b898ff9ac8cbe17ee76129/grpcio-1.75.1-cp314-cp314-linux_armv7l.whl", hash = "sha256:c05da79068dd96723793bffc8d0e64c45f316248417515f28d22204d9dae51c7", size = 5673319, upload-time = "2025-09-26T09:02:44.742Z" }, + { url = "https://files.pythonhosted.org/packages/c6/ec/9d6959429a83fbf5df8549c591a8a52bb313976f6646b79852c4884e3225/grpcio-1.75.1-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:06373a94fd16ec287116a825161dca179a0402d0c60674ceeec8c9fba344fe66", size = 11480347, upload-time = "2025-09-26T09:02:47.539Z" }, + { url = "https://files.pythonhosted.org/packages/09/7a/26da709e42c4565c3d7bf999a9569da96243ce34a8271a968dee810a7cf1/grpcio-1.75.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4484f4b7287bdaa7a5b3980f3c7224c3c622669405d20f69549f5fb956ad0421", size = 6254706, upload-time = "2025-09-26T09:02:50.4Z" }, + { url = "https://files.pythonhosted.org/packages/f1/08/dcb26a319d3725f199c97e671d904d84ee5680de57d74c566a991cfab632/grpcio-1.75.1-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:2720c239c1180eee69f7883c1d4c83fc1a495a2535b5fa322887c70bf02b16e8", size = 6922501, upload-time = "2025-09-26T09:02:52.711Z" }, + { url = "https://files.pythonhosted.org/packages/78/66/044d412c98408a5e23cb348845979a2d17a2e2b6c3c34c1ec91b920f49d0/grpcio-1.75.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:07a554fa31c668cf0e7a188678ceeca3cb8fead29bbe455352e712ec33ca701c", size = 6437492, upload-time = "2025-09-26T09:02:55.542Z" }, + { url = "https://files.pythonhosted.org/packages/4e/9d/5e3e362815152aa1afd8b26ea613effa005962f9da0eec6e0e4527e7a7d1/grpcio-1.75.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:3e71a2105210366bfc398eef7f57a664df99194f3520edb88b9c3a7e46ee0d64", size = 7081061, upload-time = "2025-09-26T09:02:58.261Z" }, + { url = "https://files.pythonhosted.org/packages/1e/1a/46615682a19e100f46e31ddba9ebc297c5a5ab9ddb47b35443ffadb8776c/grpcio-1.75.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:8679aa8a5b67976776d3c6b0521e99d1c34db8a312a12bcfd78a7085cb9b604e", size = 8010849, upload-time = "2025-09-26T09:03:00.548Z" }, + { url = "https://files.pythonhosted.org/packages/67/8e/3204b94ac30b0f675ab1c06540ab5578660dc8b690db71854d3116f20d00/grpcio-1.75.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:aad1c774f4ebf0696a7f148a56d39a3432550612597331792528895258966dc0", size = 7464478, upload-time = "2025-09-26T09:03:03.096Z" }, + { url = "https://files.pythonhosted.org/packages/b7/97/2d90652b213863b2cf466d9c1260ca7e7b67a16780431b3eb1d0420e3d5b/grpcio-1.75.1-cp314-cp314-win32.whl", hash = "sha256:62ce42d9994446b307649cb2a23335fa8e927f7ab2cbf5fcb844d6acb4d85f9c", size = 4012672, upload-time = "2025-09-26T09:03:05.477Z" }, + { url = "https://files.pythonhosted.org/packages/f9/df/e2e6e9fc1c985cd1a59e6996a05647c720fe8a03b92f5ec2d60d366c531e/grpcio-1.75.1-cp314-cp314-win_amd64.whl", hash = "sha256:f86e92275710bea3000cb79feca1762dc0ad3b27830dd1a74e82ab321d4ee464", size = 4772475, upload-time = "2025-09-26T09:03:07.661Z" }, +] + +[[package]] +name = "grpcio-tools" +version = "1.75.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "grpcio" }, + { name = "protobuf" }, + { name = "setuptools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7d/76/0cd2a2bb379275c319544a3ab613dc3cea7a167503908c1b4de55f82bd9e/grpcio_tools-1.75.1.tar.gz", hash = "sha256:bb78960cf3d58941e1fec70cbdaccf255918beed13c34112a6915a6d8facebd1", size = 5390470, upload-time = "2025-09-26T09:10:11.948Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/47/fa/624bbe1b2ccf4f6044bf3cd314fe2c35f78f702fcc2191dc65519baddca4/grpcio_tools-1.75.1-cp313-cp313-linux_armv7l.whl", hash = "sha256:ca9e116aab0ecf4365fc2980f2e8ae1b22273c3847328b9a8e05cbd14345b397", size = 2545752, upload-time = "2025-09-26T09:08:51.433Z" }, + { url = "https://files.pythonhosted.org/packages/b9/4c/6d884e2337feff0a656e395338019adecc3aa1daeae9d7e8eb54340d4207/grpcio_tools-1.75.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:9fe87a926b65eb7f41f8738b6d03677cc43185ff77a9d9b201bdb2f673f3fa1e", size = 5838163, upload-time = "2025-09-26T09:08:53.858Z" }, + { url = "https://files.pythonhosted.org/packages/d1/2a/2ba7b6911a754719643ed92ae816a7f989af2be2882b9a9e1f90f4b0e882/grpcio_tools-1.75.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:45503a6094f91b3fd31c3d9adef26ac514f102086e2a37de797e220a6791ee87", size = 2592148, upload-time = "2025-09-26T09:08:55.86Z" }, + { url = "https://files.pythonhosted.org/packages/88/db/fa613a45c3c7b00f905bd5ad3a93c73194724d0a2dd72adae3be32983343/grpcio_tools-1.75.1-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:b01b60b3de67be531a39fd869d7613fa8f178aff38c05e4d8bc2fc530fa58cb5", size = 2905215, upload-time = "2025-09-26T09:08:58.27Z" }, + { url = "https://files.pythonhosted.org/packages/d7/0c/ee4786972bb82f60e4f313bb2227c79c2cd20eb13c94c0263067923cfd12/grpcio_tools-1.75.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:09e2b9b9488735514777d44c1e4eda813122d2c87aad219f98d5d49b359a8eab", size = 2656251, upload-time = "2025-09-26T09:09:00.249Z" }, + { url = "https://files.pythonhosted.org/packages/77/f1/cc5a50658d705d0b71ff8a4fbbfcc6279d3c95731a2ef7285e13dc40e2fe/grpcio_tools-1.75.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:55e60300e62b220fabe6f062fe69f143abaeff3335f79b22b56d86254f3c3c80", size = 3108911, upload-time = "2025-09-26T09:09:02.515Z" }, + { url = "https://files.pythonhosted.org/packages/09/d8/43545f77c4918e778e90bc2c02b3462ac71cee14f29d85cdb69b089538eb/grpcio_tools-1.75.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:49ce00fcc6facbbf52bf376e55b8e08810cecd03dab0b3a2986d73117c6f6ee4", size = 3657021, upload-time = "2025-09-26T09:09:05.331Z" }, + { url = "https://files.pythonhosted.org/packages/fc/0b/2ae5925374b66bc8df5b828eff1a5f9459349c83dae1773f0aa9858707e6/grpcio_tools-1.75.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:71e95479aea868f8c8014d9dc4267f26ee75388a0d8a552e1648cfa0b53d24b4", size = 3324450, upload-time = "2025-09-26T09:09:07.867Z" }, + { url = "https://files.pythonhosted.org/packages/6e/53/9f887bacbecf892ac5b0b282477ca8cfa5b73911b04259f0d88b52e9a055/grpcio_tools-1.75.1-cp313-cp313-win32.whl", hash = "sha256:fff9d2297416eae8861e53154ccf70a19994e5935e6c8f58ebf431f81cbd8d12", size = 992434, upload-time = "2025-09-26T09:09:09.966Z" }, + { url = "https://files.pythonhosted.org/packages/a5/f0/9979d97002edffdc2a88e5f2e0dccea396dd4a6eab34fa2f705fe43eae2f/grpcio_tools-1.75.1-cp313-cp313-win_amd64.whl", hash = "sha256:1849ddd508143eb48791e81d42ddc924c554d1b4900e06775a927573a8d4267f", size = 1157069, upload-time = "2025-09-26T09:09:12.287Z" }, + { url = "https://files.pythonhosted.org/packages/a6/0b/4ff4ead293f2b016668628a240937828444094778c8037d2bbef700e9097/grpcio_tools-1.75.1-cp314-cp314-linux_armv7l.whl", hash = "sha256:f281b594489184b1f9a337cdfed1fc1ddb8428f41c4b4023de81527e90b38e1e", size = 2545868, upload-time = "2025-09-26T09:09:14.716Z" }, + { url = "https://files.pythonhosted.org/packages/0e/78/aa6bf73a18de5357c01ef87eea92150931586b25196fa4df197a37bae11d/grpcio_tools-1.75.1-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:becf8332f391abc62bf4eea488b63be063d76a7cf2ef00b2e36c617d9ee9216b", size = 5838010, upload-time = "2025-09-26T09:09:20.415Z" }, + { url = "https://files.pythonhosted.org/packages/99/65/7eaad673bc971af45e079d3b13c20d9ba9842b8788d31953e3234c2e2cee/grpcio_tools-1.75.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a08330f24e5cd7b39541882a95a8ba04ffb4df79e2984aa0cd01ed26dcdccf49", size = 2593170, upload-time = "2025-09-26T09:09:22.889Z" }, + { url = "https://files.pythonhosted.org/packages/e4/db/57e1e29e9186c7ed223ce8a9b609d3f861c4db015efb643dfe60b403c137/grpcio_tools-1.75.1-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:6bf3742bd8f102630072ed317d1496f31c454cd85ad19d37a68bd85bf9d5f8b9", size = 2905167, upload-time = "2025-09-26T09:09:25.96Z" }, + { url = "https://files.pythonhosted.org/packages/cd/7b/894f891f3cf19812192f8bbf1e0e1c958055676ecf0a5466a350730a006d/grpcio_tools-1.75.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f26028949474feb380460ce52d9d090d00023940c65236294a66c42ac5850e8b", size = 2656210, upload-time = "2025-09-26T09:09:28.786Z" }, + { url = "https://files.pythonhosted.org/packages/99/76/8e48427da93ef243c09629969c7b5a2c59dceb674b6b623c1f5fbaa5c8c5/grpcio_tools-1.75.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1bd68fb98bf08f11b6c3210834a14eefe585bad959bdba38e78b4ae3b04ba5bd", size = 3109226, upload-time = "2025-09-26T09:09:31.307Z" }, + { url = "https://files.pythonhosted.org/packages/b3/7e/ecf71c316c2a88c2478b7c6372d0f82d05f07edbf0f31b6da613df99ec7c/grpcio_tools-1.75.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:f1496e21586193da62c3a73cd16f9c63c5b3efd68ff06dab96dbdfefa90d40bf", size = 3657139, upload-time = "2025-09-26T09:09:35.043Z" }, + { url = "https://files.pythonhosted.org/packages/6f/f3/b2613e81da2085f40a989c0601ec9efc11e8b32fcb71b1234b64a18af830/grpcio_tools-1.75.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:14a78b1e36310cdb3516cdf9ee2726107875e0b247e2439d62fc8dc38cf793c1", size = 3324513, upload-time = "2025-09-26T09:09:37.44Z" }, + { url = "https://files.pythonhosted.org/packages/9a/1f/2df4fa8634542524bc22442ffe045d41905dae62cc5dd14408b80c5ac1b8/grpcio_tools-1.75.1-cp314-cp314-win32.whl", hash = "sha256:0e6f916daf222002fb98f9a6f22de0751959e7e76a24941985cc8e43cea77b50", size = 1015283, upload-time = "2025-09-26T09:09:39.461Z" }, + { url = "https://files.pythonhosted.org/packages/23/4f/f27c973ff50486a70be53a3978b6b0244398ca170a4e19d91988b5295d92/grpcio_tools-1.75.1-cp314-cp314-win_amd64.whl", hash = "sha256:878c3b362264588c45eba57ce088755f8b2b54893d41cc4a68cdeea62996da5c", size = 1189364, upload-time = "2025-09-26T09:09:42.036Z" }, +] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, +] + +[[package]] +name = "numpy" +version = "2.3.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d0/19/95b3d357407220ed24c139018d2518fab0a61a948e68286a25f1a4d049ff/numpy-2.3.3.tar.gz", hash = "sha256:ddc7c39727ba62b80dfdbedf400d1c10ddfa8eefbd7ec8dcb118be8b56d31029", size = 20576648, upload-time = "2025-09-09T16:54:12.543Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7d/b9/984c2b1ee61a8b803bf63582b4ac4242cf76e2dbd663efeafcb620cc0ccb/numpy-2.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f5415fb78995644253370985342cd03572ef8620b934da27d77377a2285955bf", size = 20949588, upload-time = "2025-09-09T15:56:59.087Z" }, + { url = "https://files.pythonhosted.org/packages/a6/e4/07970e3bed0b1384d22af1e9912527ecbeb47d3b26e9b6a3bced068b3bea/numpy-2.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d00de139a3324e26ed5b95870ce63be7ec7352171bc69a4cf1f157a48e3eb6b7", size = 14177802, upload-time = "2025-09-09T15:57:01.73Z" }, + { url = "https://files.pythonhosted.org/packages/35/c7/477a83887f9de61f1203bad89cf208b7c19cc9fef0cebef65d5a1a0619f2/numpy-2.3.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:9dc13c6a5829610cc07422bc74d3ac083bd8323f14e2827d992f9e52e22cd6a6", size = 5106537, upload-time = "2025-09-09T15:57:03.765Z" }, + { url = "https://files.pythonhosted.org/packages/52/47/93b953bd5866a6f6986344d045a207d3f1cfbad99db29f534ea9cee5108c/numpy-2.3.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:d79715d95f1894771eb4e60fb23f065663b2298f7d22945d66877aadf33d00c7", size = 6640743, upload-time = "2025-09-09T15:57:07.921Z" }, + { url = "https://files.pythonhosted.org/packages/23/83/377f84aaeb800b64c0ef4de58b08769e782edcefa4fea712910b6f0afd3c/numpy-2.3.3-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:952cfd0748514ea7c3afc729a0fc639e61655ce4c55ab9acfab14bda4f402b4c", size = 14278881, upload-time = "2025-09-09T15:57:11.349Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a5/bf3db6e66c4b160d6ea10b534c381a1955dfab34cb1017ea93aa33c70ed3/numpy-2.3.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5b83648633d46f77039c29078751f80da65aa64d5622a3cd62aaef9d835b6c93", size = 16636301, upload-time = "2025-09-09T15:57:14.245Z" }, + { url = "https://files.pythonhosted.org/packages/a2/59/1287924242eb4fa3f9b3a2c30400f2e17eb2707020d1c5e3086fe7330717/numpy-2.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b001bae8cea1c7dfdb2ae2b017ed0a6f2102d7a70059df1e338e307a4c78a8ae", size = 16053645, upload-time = "2025-09-09T15:57:16.534Z" }, + { url = "https://files.pythonhosted.org/packages/e6/93/b3d47ed882027c35e94ac2320c37e452a549f582a5e801f2d34b56973c97/numpy-2.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8e9aced64054739037d42fb84c54dd38b81ee238816c948c8f3ed134665dcd86", size = 18578179, upload-time = "2025-09-09T15:57:18.883Z" }, + { url = "https://files.pythonhosted.org/packages/20/d9/487a2bccbf7cc9d4bfc5f0f197761a5ef27ba870f1e3bbb9afc4bbe3fcc2/numpy-2.3.3-cp313-cp313-win32.whl", hash = "sha256:9591e1221db3f37751e6442850429b3aabf7026d3b05542d102944ca7f00c8a8", size = 6312250, upload-time = "2025-09-09T15:57:21.296Z" }, + { url = "https://files.pythonhosted.org/packages/1b/b5/263ebbbbcede85028f30047eab3d58028d7ebe389d6493fc95ae66c636ab/numpy-2.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:f0dadeb302887f07431910f67a14d57209ed91130be0adea2f9793f1a4f817cf", size = 12783269, upload-time = "2025-09-09T15:57:23.034Z" }, + { url = "https://files.pythonhosted.org/packages/fa/75/67b8ca554bbeaaeb3fac2e8bce46967a5a06544c9108ec0cf5cece559b6c/numpy-2.3.3-cp313-cp313-win_arm64.whl", hash = "sha256:3c7cf302ac6e0b76a64c4aecf1a09e51abd9b01fc7feee80f6c43e3ab1b1dbc5", size = 10195314, upload-time = "2025-09-09T15:57:25.045Z" }, + { url = "https://files.pythonhosted.org/packages/11/d0/0d1ddec56b162042ddfafeeb293bac672de9b0cfd688383590090963720a/numpy-2.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:eda59e44957d272846bb407aad19f89dc6f58fecf3504bd144f4c5cf81a7eacc", size = 21048025, upload-time = "2025-09-09T15:57:27.257Z" }, + { url = "https://files.pythonhosted.org/packages/36/9e/1996ca6b6d00415b6acbdd3c42f7f03ea256e2c3f158f80bd7436a8a19f3/numpy-2.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:823d04112bc85ef5c4fda73ba24e6096c8f869931405a80aa8b0e604510a26bc", size = 14301053, upload-time = "2025-09-09T15:57:30.077Z" }, + { url = "https://files.pythonhosted.org/packages/05/24/43da09aa764c68694b76e84b3d3f0c44cb7c18cdc1ba80e48b0ac1d2cd39/numpy-2.3.3-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:40051003e03db4041aa325da2a0971ba41cf65714e65d296397cc0e32de6018b", size = 5229444, upload-time = "2025-09-09T15:57:32.733Z" }, + { url = "https://files.pythonhosted.org/packages/bc/14/50ffb0f22f7218ef8af28dd089f79f68289a7a05a208db9a2c5dcbe123c1/numpy-2.3.3-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:6ee9086235dd6ab7ae75aba5662f582a81ced49f0f1c6de4260a78d8f2d91a19", size = 6738039, upload-time = "2025-09-09T15:57:34.328Z" }, + { url = "https://files.pythonhosted.org/packages/55/52/af46ac0795e09657d45a7f4db961917314377edecf66db0e39fa7ab5c3d3/numpy-2.3.3-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:94fcaa68757c3e2e668ddadeaa86ab05499a70725811e582b6a9858dd472fb30", size = 14352314, upload-time = "2025-09-09T15:57:36.255Z" }, + { url = "https://files.pythonhosted.org/packages/a7/b1/dc226b4c90eb9f07a3fff95c2f0db3268e2e54e5cce97c4ac91518aee71b/numpy-2.3.3-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:da1a74b90e7483d6ce5244053399a614b1d6b7bc30a60d2f570e5071f8959d3e", size = 16701722, upload-time = "2025-09-09T15:57:38.622Z" }, + { url = "https://files.pythonhosted.org/packages/9d/9d/9d8d358f2eb5eced14dba99f110d83b5cd9a4460895230f3b396ad19a323/numpy-2.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2990adf06d1ecee3b3dcbb4977dfab6e9f09807598d647f04d385d29e7a3c3d3", size = 16132755, upload-time = "2025-09-09T15:57:41.16Z" }, + { url = "https://files.pythonhosted.org/packages/b6/27/b3922660c45513f9377b3fb42240bec63f203c71416093476ec9aa0719dc/numpy-2.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:ed635ff692483b8e3f0fcaa8e7eb8a75ee71aa6d975388224f70821421800cea", size = 18651560, upload-time = "2025-09-09T15:57:43.459Z" }, + { url = "https://files.pythonhosted.org/packages/5b/8e/3ab61a730bdbbc201bb245a71102aa609f0008b9ed15255500a99cd7f780/numpy-2.3.3-cp313-cp313t-win32.whl", hash = "sha256:a333b4ed33d8dc2b373cc955ca57babc00cd6f9009991d9edc5ddbc1bac36bcd", size = 6442776, upload-time = "2025-09-09T15:57:45.793Z" }, + { url = "https://files.pythonhosted.org/packages/1c/3a/e22b766b11f6030dc2decdeff5c2fb1610768055603f9f3be88b6d192fb2/numpy-2.3.3-cp313-cp313t-win_amd64.whl", hash = "sha256:4384a169c4d8f97195980815d6fcad04933a7e1ab3b530921c3fef7a1c63426d", size = 12927281, upload-time = "2025-09-09T15:57:47.492Z" }, + { url = "https://files.pythonhosted.org/packages/7b/42/c2e2bc48c5e9b2a83423f99733950fbefd86f165b468a3d85d52b30bf782/numpy-2.3.3-cp313-cp313t-win_arm64.whl", hash = "sha256:75370986cc0bc66f4ce5110ad35aae6d182cc4ce6433c40ad151f53690130bf1", size = 10265275, upload-time = "2025-09-09T15:57:49.647Z" }, + { url = "https://files.pythonhosted.org/packages/6b/01/342ad585ad82419b99bcf7cebe99e61da6bedb89e213c5fd71acc467faee/numpy-2.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:cd052f1fa6a78dee696b58a914b7229ecfa41f0a6d96dc663c1220a55e137593", size = 20951527, upload-time = "2025-09-09T15:57:52.006Z" }, + { url = "https://files.pythonhosted.org/packages/ef/d8/204e0d73fc1b7a9ee80ab1fe1983dd33a4d64a4e30a05364b0208e9a241a/numpy-2.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:414a97499480067d305fcac9716c29cf4d0d76db6ebf0bf3cbce666677f12652", size = 14186159, upload-time = "2025-09-09T15:57:54.407Z" }, + { url = "https://files.pythonhosted.org/packages/22/af/f11c916d08f3a18fb8ba81ab72b5b74a6e42ead4c2846d270eb19845bf74/numpy-2.3.3-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:50a5fe69f135f88a2be9b6ca0481a68a136f6febe1916e4920e12f1a34e708a7", size = 5114624, upload-time = "2025-09-09T15:57:56.5Z" }, + { url = "https://files.pythonhosted.org/packages/fb/11/0ed919c8381ac9d2ffacd63fd1f0c34d27e99cab650f0eb6f110e6ae4858/numpy-2.3.3-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:b912f2ed2b67a129e6a601e9d93d4fa37bef67e54cac442a2f588a54afe5c67a", size = 6642627, upload-time = "2025-09-09T15:57:58.206Z" }, + { url = "https://files.pythonhosted.org/packages/ee/83/deb5f77cb0f7ba6cb52b91ed388b47f8f3c2e9930d4665c600408d9b90b9/numpy-2.3.3-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9e318ee0596d76d4cb3d78535dc005fa60e5ea348cd131a51e99d0bdbe0b54fe", size = 14296926, upload-time = "2025-09-09T15:58:00.035Z" }, + { url = "https://files.pythonhosted.org/packages/77/cc/70e59dcb84f2b005d4f306310ff0a892518cc0c8000a33d0e6faf7ca8d80/numpy-2.3.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ce020080e4a52426202bdb6f7691c65bb55e49f261f31a8f506c9f6bc7450421", size = 16638958, upload-time = "2025-09-09T15:58:02.738Z" }, + { url = "https://files.pythonhosted.org/packages/b6/5a/b2ab6c18b4257e099587d5b7f903317bd7115333ad8d4ec4874278eafa61/numpy-2.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e6687dc183aa55dae4a705b35f9c0f8cb178bcaa2f029b241ac5356221d5c021", size = 16071920, upload-time = "2025-09-09T15:58:05.029Z" }, + { url = "https://files.pythonhosted.org/packages/b8/f1/8b3fdc44324a259298520dd82147ff648979bed085feeacc1250ef1656c0/numpy-2.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d8f3b1080782469fdc1718c4ed1d22549b5fb12af0d57d35e992158a772a37cf", size = 18577076, upload-time = "2025-09-09T15:58:07.745Z" }, + { url = "https://files.pythonhosted.org/packages/f0/a1/b87a284fb15a42e9274e7fcea0dad259d12ddbf07c1595b26883151ca3b4/numpy-2.3.3-cp314-cp314-win32.whl", hash = "sha256:cb248499b0bc3be66ebd6578b83e5acacf1d6cb2a77f2248ce0e40fbec5a76d0", size = 6366952, upload-time = "2025-09-09T15:58:10.096Z" }, + { url = "https://files.pythonhosted.org/packages/70/5f/1816f4d08f3b8f66576d8433a66f8fa35a5acfb3bbd0bf6c31183b003f3d/numpy-2.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:691808c2b26b0f002a032c73255d0bd89751425f379f7bcd22d140db593a96e8", size = 12919322, upload-time = "2025-09-09T15:58:12.138Z" }, + { url = "https://files.pythonhosted.org/packages/8c/de/072420342e46a8ea41c324a555fa90fcc11637583fb8df722936aed1736d/numpy-2.3.3-cp314-cp314-win_arm64.whl", hash = "sha256:9ad12e976ca7b10f1774b03615a2a4bab8addce37ecc77394d8e986927dc0dfe", size = 10478630, upload-time = "2025-09-09T15:58:14.64Z" }, + { url = "https://files.pythonhosted.org/packages/d5/df/ee2f1c0a9de7347f14da5dd3cd3c3b034d1b8607ccb6883d7dd5c035d631/numpy-2.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9cc48e09feb11e1db00b320e9d30a4151f7369afb96bd0e48d942d09da3a0d00", size = 21047987, upload-time = "2025-09-09T15:58:16.889Z" }, + { url = "https://files.pythonhosted.org/packages/d6/92/9453bdc5a4e9e69cf4358463f25e8260e2ffc126d52e10038b9077815989/numpy-2.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:901bf6123879b7f251d3631967fd574690734236075082078e0571977c6a8e6a", size = 14301076, upload-time = "2025-09-09T15:58:20.343Z" }, + { url = "https://files.pythonhosted.org/packages/13/77/1447b9eb500f028bb44253105bd67534af60499588a5149a94f18f2ca917/numpy-2.3.3-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:7f025652034199c301049296b59fa7d52c7e625017cae4c75d8662e377bf487d", size = 5229491, upload-time = "2025-09-09T15:58:22.481Z" }, + { url = "https://files.pythonhosted.org/packages/3d/f9/d72221b6ca205f9736cb4b2ce3b002f6e45cd67cd6a6d1c8af11a2f0b649/numpy-2.3.3-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:533ca5f6d325c80b6007d4d7fb1984c303553534191024ec6a524a4c92a5935a", size = 6737913, upload-time = "2025-09-09T15:58:24.569Z" }, + { url = "https://files.pythonhosted.org/packages/3c/5f/d12834711962ad9c46af72f79bb31e73e416ee49d17f4c797f72c96b6ca5/numpy-2.3.3-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0edd58682a399824633b66885d699d7de982800053acf20be1eaa46d92009c54", size = 14352811, upload-time = "2025-09-09T15:58:26.416Z" }, + { url = "https://files.pythonhosted.org/packages/a1/0d/fdbec6629d97fd1bebed56cd742884e4eead593611bbe1abc3eb40d304b2/numpy-2.3.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:367ad5d8fbec5d9296d18478804a530f1191e24ab4d75ab408346ae88045d25e", size = 16702689, upload-time = "2025-09-09T15:58:28.831Z" }, + { url = "https://files.pythonhosted.org/packages/9b/09/0a35196dc5575adde1eb97ddfbc3e1687a814f905377621d18ca9bc2b7dd/numpy-2.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8f6ac61a217437946a1fa48d24c47c91a0c4f725237871117dea264982128097", size = 16133855, upload-time = "2025-09-09T15:58:31.349Z" }, + { url = "https://files.pythonhosted.org/packages/7a/ca/c9de3ea397d576f1b6753eaa906d4cdef1bf97589a6d9825a349b4729cc2/numpy-2.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:179a42101b845a816d464b6fe9a845dfaf308fdfc7925387195570789bb2c970", size = 18652520, upload-time = "2025-09-09T15:58:33.762Z" }, + { url = "https://files.pythonhosted.org/packages/fd/c2/e5ed830e08cd0196351db55db82f65bc0ab05da6ef2b72a836dcf1936d2f/numpy-2.3.3-cp314-cp314t-win32.whl", hash = "sha256:1250c5d3d2562ec4174bce2e3a1523041595f9b651065e4a4473f5f48a6bc8a5", size = 6515371, upload-time = "2025-09-09T15:58:36.04Z" }, + { url = "https://files.pythonhosted.org/packages/47/c7/b0f6b5b67f6788a0725f744496badbb604d226bf233ba716683ebb47b570/numpy-2.3.3-cp314-cp314t-win_amd64.whl", hash = "sha256:b37a0b2e5935409daebe82c1e42274d30d9dd355852529eab91dab8dcca7419f", size = 13112576, upload-time = "2025-09-09T15:58:37.927Z" }, + { url = "https://files.pythonhosted.org/packages/06/b9/33bba5ff6fb679aa0b1f8a07e853f002a6b04b9394db3069a1270a7784ca/numpy-2.3.3-cp314-cp314t-win_arm64.whl", hash = "sha256:78c9f6560dc7e6b3990e32df7ea1a50bbd0e2a111e05209963f5ddcab7073b0b", size = 10545953, upload-time = "2025-09-09T15:58:40.576Z" }, +] + +[[package]] +name = "packaging" +version = "25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, +] + +[[package]] +name = "pathspec" +version = "0.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043, upload-time = "2023-12-10T22:30:45Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191, upload-time = "2023-12-10T22:30:43.14Z" }, +] + +[[package]] +name = "plac" +version = "1.4.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/23/09/26ef2d614cabdcc52a7f383d0dc7967bf46be3c9700898c594e37b710c3d/plac-1.4.5.tar.gz", hash = "sha256:5f05bf85235c017fcd76c73c8101d4ff8e96beb3dc58b9a37de49cac7de82d14", size = 38988, upload-time = "2025-04-04T14:03:25.651Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/15/36/38676114a0dbee137ec366daa86603d667a07e9a52667d5ebf5c580100ba/plac-1.4.5-py2.py3-none-any.whl", hash = "sha256:87187786b4e446688b1cf5112e18fed8a23ab3b316c25fe91266a10bd1736b16", size = 22468, upload-time = "2025-04-04T14:03:24.761Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/61/33/9611380c2bdb1225fdef633e2a9610622310fed35ab11dac9620972ee088/platformdirs-4.5.0.tar.gz", hash = "sha256:70ddccdd7c99fc5942e9fc25636a8b34d04c24b335100223152c2803e4063312", size = 21632, upload-time = "2025-10-08T17:44:48.791Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/73/cb/ac7874b3e5d58441674fb70742e6c374b28b0c7cb988d37d991cde47166c/platformdirs-4.5.0-py3-none-any.whl", hash = "sha256:e578a81bb873cbb89a41fcc904c7ef523cc18284b7e3b3ccf06aca1403b7ebd3", size = 18651, upload-time = "2025-10-08T17:44:47.223Z" }, +] + +[[package]] +name = "protobuf" +version = "6.32.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fa/a4/cc17347aa2897568beece2e674674359f911d6fe21b0b8d6268cd42727ac/protobuf-6.32.1.tar.gz", hash = "sha256:ee2469e4a021474ab9baafea6cd070e5bf27c7d29433504ddea1a4ee5850f68d", size = 440635, upload-time = "2025-09-11T21:38:42.935Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c0/98/645183ea03ab3995d29086b8bf4f7562ebd3d10c9a4b14ee3f20d47cfe50/protobuf-6.32.1-cp310-abi3-win32.whl", hash = "sha256:a8a32a84bc9f2aad712041b8b366190f71dde248926da517bde9e832e4412085", size = 424411, upload-time = "2025-09-11T21:38:27.427Z" }, + { url = "https://files.pythonhosted.org/packages/8c/f3/6f58f841f6ebafe076cebeae33fc336e900619d34b1c93e4b5c97a81fdfa/protobuf-6.32.1-cp310-abi3-win_amd64.whl", hash = "sha256:b00a7d8c25fa471f16bc8153d0e53d6c9e827f0953f3c09aaa4331c718cae5e1", size = 435738, upload-time = "2025-09-11T21:38:30.959Z" }, + { url = "https://files.pythonhosted.org/packages/10/56/a8a3f4e7190837139e68c7002ec749190a163af3e330f65d90309145a210/protobuf-6.32.1-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:d8c7e6eb619ffdf105ee4ab76af5a68b60a9d0f66da3ea12d1640e6d8dab7281", size = 426454, upload-time = "2025-09-11T21:38:34.076Z" }, + { url = "https://files.pythonhosted.org/packages/3f/be/8dd0a927c559b37d7a6c8ab79034fd167dcc1f851595f2e641ad62be8643/protobuf-6.32.1-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:2f5b80a49e1eb7b86d85fcd23fe92df154b9730a725c3b38c4e43b9d77018bf4", size = 322874, upload-time = "2025-09-11T21:38:35.509Z" }, + { url = "https://files.pythonhosted.org/packages/5c/f6/88d77011b605ef979aace37b7703e4eefad066f7e84d935e5a696515c2dd/protobuf-6.32.1-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:b1864818300c297265c83a4982fd3169f97122c299f56a56e2445c3698d34710", size = 322013, upload-time = "2025-09-11T21:38:37.017Z" }, + { url = "https://files.pythonhosted.org/packages/97/b7/15cc7d93443d6c6a84626ae3258a91f4c6ac8c0edd5df35ea7658f71b79c/protobuf-6.32.1-py3-none-any.whl", hash = "sha256:2601b779fc7d32a866c6b4404f9d42a3f67c5b9f3f15b4db3cccabe06b95c346", size = 169289, upload-time = "2025-09-11T21:38:41.234Z" }, +] + +[[package]] +name = "pytokens" +version = "0.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d4/c2/dbadcdddb412a267585459142bfd7cc241e6276db69339353ae6e241ab2b/pytokens-0.2.0.tar.gz", hash = "sha256:532d6421364e5869ea57a9523bf385f02586d4662acbcc0342afd69511b4dd43", size = 15368, upload-time = "2025-10-15T08:02:42.738Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/89/5a/c269ea6b348b6f2c32686635df89f32dbe05df1088dd4579302a6f8f99af/pytokens-0.2.0-py3-none-any.whl", hash = "sha256:74d4b318c67f4295c13782ddd9abcb7e297ec5630ad060eb90abf7ebbefe59f8", size = 12038, upload-time = "2025-10-15T08:02:41.694Z" }, +] + +[[package]] +name = "setuptools" +version = "80.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/18/5d/3bf57dcd21979b887f014ea83c24ae194cfcd12b9e0fda66b957c69d1fca/setuptools-80.9.0.tar.gz", hash = "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c", size = 1319958, upload-time = "2025-05-27T00:56:51.443Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922", size = 1201486, upload-time = "2025-05-27T00:56:49.664Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] diff --git a/test/runtests.jl b/test/runtests.jl index 2fbc674..f7a563c 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,6 +1,222 @@ -using Test +using Test +using ProtoBuf +using gRPCClient +using Base.Threads -@testset "gRPCClient" begin - include("runtests_routeguide.jl") - include("runtests_errors.jl") + +function _get_test_host() + if "GRPC_TEST_SERVER_HOST" in keys(ENV) + ENV["GRPC_TEST_SERVER_HOST"] + else + "localhost" + end +end + +function _get_test_port() + if "GRPC_TEST_SERVER_PORT" in keys(ENV) + parse(UInt16, ENV["GRPC_TEST_SERVER_PORT"]) + else + 8001 + end end + +const _TEST_HOST = _get_test_host() +const _TEST_PORT = _get_test_port() + +# protobuf and service definitions for our tests +include("gen/test/test_pb.jl") + +@testset "gRPCClient.jl" begin + + # Initialize the global gRPCCURL structure + grpc_init() + + # @testset "@async varying request/response" begin + client = TestService_TestRPC_Client(_TEST_HOST, _TEST_PORT) + + requests = Vector{gRPCRequest}() + for i in 1:1000 + request = grpc_async_request(client, TestRequest(i, zeros(UInt64, i))) + push!(requests, request) + end + + for (i, request) in enumerate(requests) + response = grpc_async_await(client, request) + @test length(response.data) == i + + for (di, dv) in enumerate(response.data) + @test di == dv + end + end + # end + + # @testset "@async small request/response" begin + client = TestService_TestRPC_Client(_TEST_HOST, _TEST_PORT) + + requests = Vector{gRPCRequest}() + for i in 1:1000 + request = grpc_async_request(client, TestRequest(1, zeros(UInt64, 1))) + push!(requests, request) + end + + for (i, request) in enumerate(requests) + response = grpc_async_await(client, request) + @test length(response.data) == 1 + @test response.data[1] == 1 + end + # end + + # @testset "@async big request/response" begin + client = TestService_TestRPC_Client(_TEST_HOST, _TEST_PORT) + + requests = Vector{gRPCRequest}() + for i in 1:100 + # 28*224*sizeof(UInt64) == sending batch of 32 224*224 UInt8 image + request = grpc_async_request(client, TestRequest(64, zeros(UInt64, 32*28*224))) + push!(requests, request) + end + + for (i, request) in enumerate(requests) + response = grpc_async_await(client, request) + @test length(response.data) == 64 + end + # end + + # @testset "Threads.@spawn small request/response" begin + client = TestService_TestRPC_Client(_TEST_HOST, _TEST_PORT) + + responses = [TestResponse(Vector{UInt64}()) for _ in 1:1000] + + @sync Threads.@threads for i in 1:1000 + response = grpc_sync_request(client, TestRequest(1, zeros(UInt64, 1))) + responses[i] = response + end + + for (i, response) in enumerate(responses) + @test length(response.data) == 1 + @test response.data[1] == 1 + end + # end + + # @testset "Threads.@spawn varying request/response" begin + client = TestService_TestRPC_Client(_TEST_HOST, _TEST_PORT) + + responses = [TestResponse(Vector{UInt64}()) for _ in 1:1000] + + @sync Threads.@threads for i in 1:1000 + response = grpc_sync_request(client, TestRequest(i, zeros(UInt64, i))) + responses[i] = response + end + + for (i, response) in enumerate(responses) + @test length(response.data) == i + for (di, dv) in enumerate(response.data) + @test di == dv + end + end + # end + + # @testset "Max Message Size" begin + # Create a client with much more restictive max message lengths + client = TestService_TestRPC_Client(_TEST_HOST, _TEST_PORT; max_send_message_length=1024, max_recieve_message_length=1024) + + # Send too much + @test_throws gRPCServiceCallException grpc_sync_request(client, TestRequest(1, zeros(UInt64, 1024))) + # Receive too much + @test_throws gRPCServiceCallException grpc_sync_request(client, TestRequest(1024, zeros(UInt64, 1))) + # end + + # @testset "Async Channels" begin + client = TestService_TestRPC_Client(_TEST_HOST, _TEST_PORT) + + channel = Channel{gRPCAsyncChannelResponse{TestResponse}}(1000) + for i in 1:1000 + grpc_async_request(client, TestRequest(i, zeros(UInt64, 1)), channel, i) + end + + for i in 1:1000 + r = take!(channel) + !isnothing(r.ex) && throw(r.ex) + @test r.index == length(r.response.data) + end + # end + + @static if VERSION >= v"1.12" + # @testset "Response Streaming" begin + N = 16 + + client = TestService_TestServerStreamRPC_Client(_TEST_HOST, _TEST_PORT) + + response_c = Channel{TestResponse}(N) + + req = grpc_async_request(client, TestRequest(16, zeros(UInt64, 1)), response_c) + + # We should get back 16 messages that end with their length + for i in 1:N + response = take!(response_c) + @test length(response.data) == i + @test last(response.data) == i + end + + grpc_async_await(req) + # end + + # @testset "Request Streaming" begin + N = 16 + client = TestService_TestClientStreamRPC_Client(_TEST_HOST, _TEST_PORT) + request_c = Channel{TestRequest}(N) + + request = grpc_async_request(client, request_c) + + for i in 1:N + put!(request_c, TestRequest(1, zeros(UInt64, 1))) + end + + close(request_c) + response = grpc_async_await(client, request) + + @test length(response.data) == N + for i in 1:N + @test response.data[i] == i + end + # end + + # @testset "Bidirectional Streaming" begin + N = 16 + client = TestService_TestBidirectionalStreamRPC_Client(_TEST_HOST, _TEST_PORT) + + request_c = Channel{TestRequest}(N) + response_c = Channel{TestResponse}(N) + + req = grpc_async_request(client, request_c, response_c) + + for i in 1:N + put!(request_c, TestRequest(i, zeros(UInt64, i))) + end + + for i in 1:N + response = take!(response_c) + @test length(response.data) == i + @test last(response.data) == i + end + + close(request_c) + grpc_async_await(req) + # end + + # @testset "Don't Stick User Tasks" + # This fails on Julia 1.10 but works on Julia 1.12 + client = TestService_TestRPC_Client(_TEST_HOST, _TEST_PORT) + + task = @sync begin + @spawn begin + grpc_sync_request(client, TestRequest(1, zeros(UInt64, 1))) + end + end + + @test !task.sticky + # end + end + + grpc_shutdown() +end \ No newline at end of file diff --git a/test/runtests_errors.jl b/test/runtests_errors.jl deleted file mode 100644 index 8fb793d..0000000 --- a/test/runtests_errors.jl +++ /dev/null @@ -1,87 +0,0 @@ -module ErrorTest - -using gRPCClient -using Downloads -using Random -using Sockets -using Test - -const SERVER_RELEASE = "https://github.com/JuliaComputing/gRPCClient.jl/releases/download/testserver_v0.2/" -function server_binary() - arch = (Sys.ARCH === :x86_64) ? "amd64" : "386" - filename = Sys.islinux() ? "grpcerrors_linux_$(arch)" : - Sys.iswindows() ? "grpcerrors_windows_$(arch).exe" : - Sys.isapple() ? "grpcerrors_darwin_$(arch)" : - error("no server binary available for this platform") - source = string(SERVER_RELEASE, filename) - destination = joinpath(@__DIR__, filename) - isfile(destination) || Downloads.download(source, destination) - ((filemode(destination) & 0o777) == 0o777) || chmod(destination, 0o777) - - destination -end - -function start_server() - serverbin = server_binary() - - @info("starting test server", serverbin) - serverproc = run(`$serverbin`; wait=false) - - listening = timedwait(120.0; pollint=5.0) do - try - sock = connect(ip"127.0.0.1", 10000) - close(sock) - true - catch - false - end - end - - if listening !== :ok - @warn("test server did not start, stopping server") - kill(serverproc) - error("test server did not start") - end - - serverproc -end - -function test_generate() - @testset "codegen" begin - dir = joinpath(@__DIR__, "GrpcerrorsClients") - gRPCClient.generate(joinpath(@__DIR__, "error_test_server", "grpcerrors", "grpcerrors.proto"); outdir=dir) - @test isfile(joinpath(dir, "GrpcerrorsClients.jl")) - @test isfile(joinpath(dir, "grpcerrors.jl")) - @test isfile(joinpath(dir, "grpcerrors_pb.jl")) - end -end - -# switch off host verification for tests -if isempty(get(ENV, "JULIA_NO_VERIFY_HOSTS", "")) - ENV["JULIA_NO_VERIFY_HOSTS"] = "**" -end - -server_endpoint = isempty(ARGS) ? "http://localhost:10000/" : ARGS[1] -@info("server endpoint: $server_endpoint") - -@testset "Server Errors" begin - if !Sys.iswindows() - test_generate() - else - @info("skipping code generation on Windows to avoid needing batch file execution permissions") - end - include("test_grpcerrors.jl") - - @info("testing connect timeouts") - test_connect_timeout() - - serverproc = start_server() - - @info("testing grpcerrors...") - test_clients(server_endpoint) - - kill(serverproc) - @info("stopped test server") -end - -end # module ErrorTest diff --git a/test/runtests_routeguide.jl b/test/runtests_routeguide.jl deleted file mode 100644 index 5af8c39..0000000 --- a/test/runtests_routeguide.jl +++ /dev/null @@ -1,100 +0,0 @@ -module RouteClientTest - -using gRPCClient -using Downloads -using Random -using Sockets -using Test - -const SERVER_RELEASE = "https://github.com/JuliaComputing/gRPCClient.jl/releases/download/testserver_v0.2/" -function server_binary() - arch = (Sys.ARCH === :x86_64) ? "amd64" : "386" - filename = Sys.islinux() ? "routeguide_linux_$(arch)" : - Sys.iswindows() ? "routeguide_windows_$(arch).exe" : - Sys.isapple() ? "routeguide_darwin_$(arch)" : - error("no server binary available for this platform") - source = string(SERVER_RELEASE, filename) - destination = joinpath(@__DIR__, filename) - isfile(destination) || Downloads.download(source, destination) - ((filemode(destination) & 0o777) == 0o777) || chmod(destination, 0o777) - - destination -end - -function start_server() - serverbin = server_binary() - - @info("starting test server", serverbin) - serverproc = run(`$serverbin`; wait=false) - - listening = timedwait(120.0; pollint=5.0) do - try - sock = connect(ip"127.0.0.1", 10000) - close(sock) - true - catch - false - end - end - - if listening !== :ok - @warn("test server did not start, stopping server") - kill(serverproc) - error("test server did not start") - end - - serverproc -end - -function test_generate() - @testset "codegen" begin - dir = joinpath(@__DIR__, "RouteguideClients") - gRPCClient.generate(joinpath(dir, "route_guide.proto"); outdir=dir) - @test isfile(joinpath(dir, "route_guide_pb.jl")) - @test isfile(joinpath(dir, "routeguide.jl")) - @test isfile(joinpath(dir, "RouteguideClients.jl")) - end -end - -function test_timeout_header_values() - @testset "timeout header" begin - @test "100S" == gRPCClient.grpc_timeout_header_val(100) - @test "100010m" == gRPCClient.grpc_timeout_header_val(100.01) - @test "100000100u" == gRPCClient.grpc_timeout_header_val(100.0001) - @test "100000010000n" == gRPCClient.grpc_timeout_header_val(100.00001) - end -end - -# switch off host verification for tests -if isempty(get(ENV, "JULIA_NO_VERIFY_HOSTS", "")) - ENV["JULIA_NO_VERIFY_HOSTS"] = "**" -end - -server_endpoint = isempty(ARGS) ? "http://localhost:10000/" : ARGS[1] -@info("server endpoint: $server_endpoint") - -@testset "RouteGuide" begin - if !Sys.iswindows() - test_generate() - else - @info("skipping code generation on Windows to avoid needing batch file execution permissions") - end - - test_timeout_header_values() - - include("test_routeclient.jl") - serverproc = start_server() - - @debug("testing routeclinet...") - test_clients(server_endpoint) - - if get(ENV, "TEST_ASYNC", "false") == "true" - @debug("testing async safety...") - test_task_safety(server_endpoint) - end - - kill(serverproc) - @info("stopped test server") -end - -end # module RouteClientTest diff --git a/test/test_grpcerrors.jl b/test/test_grpcerrors.jl deleted file mode 100644 index f8899a3..0000000 --- a/test/test_grpcerrors.jl +++ /dev/null @@ -1,173 +0,0 @@ -include("GrpcerrorsClients/GrpcerrorsClients.jl") -using .GrpcerrorsClients - -# GrpcerrorsClients.Data mode values: -# 1: throw an error after seconds provided in `param` -# 2: no error, just wait until seconds provided in `param`, respond with SimulationParams -Base.show(io::IO, data::GrpcerrorsClients.Data) = print(io, string("[", data.mode, ", ", data.param, "]")) - -# single request, single response -function test_simplerpc(client::GRPCErrorsBlockingClient) - data = GrpcerrorsClients.Data(; mode=1, param=0) - try - _, status_future = GrpcerrorsClients.SimpleRPC(client, data) - gRPCCheck(status_future) - error("error not caught") - catch ex - @test isa(ex, gRPCServiceCallException) - @test ex.message == "simulated error mode 1" - @test ex.grpc_status == StatusCode.UNKNOWN.code - end - - data = GrpcerrorsClients.Data(; mode=2, param=5) - try - _, status_future = GrpcerrorsClients.SimpleRPC(client, data) - gRPCCheck(status_future) - error("error not caught") - catch ex - @test isa(ex, gRPCServiceCallException) - @test ex.message == StatusCode.DEADLINE_EXCEEDED.message - @test ex.grpc_status == StatusCode.DEADLINE_EXCEEDED.code - end -end - -# single request, stream response -function test_stream_response(client::GRPCErrorsBlockingClient) - data = GrpcerrorsClients.Data(; mode=1, param=0) - try - _, status_future = GrpcerrorsClients.StreamResponse(client, data) - gRPCCheck(status_future) - error("error not caught") - catch ex - @test isa(ex, gRPCServiceCallException) - @test ex.message == "simulated error mode 1" - @test ex.grpc_status == StatusCode.UNKNOWN.code - end - - data = GrpcerrorsClients.Data(; mode=2, param=25) - try - _, status_future = GrpcerrorsClients.StreamResponse(client, data) - gRPCCheck(status_future) - error("error not caught") - catch ex - @test isa(ex, gRPCServiceCallException) - @test ex.message == StatusCode.DEADLINE_EXCEEDED.message - @test ex.grpc_status == StatusCode.DEADLINE_EXCEEDED.code - end -end - -# stream request, single response -function test_stream_request(client::GRPCErrorsBlockingClient) - try - data = GrpcerrorsClients.Data(; mode=1, param=0) - in = Channel{GrpcerrorsClients.Data}(1) - @async begin - put!(in, data) - close(in) - end - _, status_future = GrpcerrorsClients.StreamRequest(client, in) - gRPCCheck(status_future) - error("error not caught") - catch ex - @test isa(ex, gRPCServiceCallException) - @test ex.message == "simulated error mode 1" - @test ex.grpc_status == StatusCode.UNKNOWN.code - end - - try - data = GrpcerrorsClients.Data(; mode=2, param=5) - in = Channel{GrpcerrorsClients.Data}(1) - @async begin - put!(in, data) - close(in) - end - _, status_future = GrpcerrorsClients.StreamRequest(client, in) - gRPCCheck(status_future) - error("error not caught") - catch ex - @test isa(ex, gRPCServiceCallException) - @test ex.message == StatusCode.DEADLINE_EXCEEDED.message - @test ex.grpc_status == StatusCode.DEADLINE_EXCEEDED.code - end -end - -# stream request, stream response -function test_stream_request_response(client::GRPCErrorsBlockingClient) - try - data = GrpcerrorsClients.Data(; mode=1, param=0) - in = Channel{GrpcerrorsClients.Data}(1) - @async begin - put!(in, data) - close(in) - end - _, status_future = GrpcerrorsClients.StreamRequestResponse(client, in) - gRPCCheck(status_future) - error("error not caught") - catch ex - @test isa(ex, gRPCServiceCallException) - @test ex.message == "simulated error mode 1" - @test ex.grpc_status == StatusCode.UNKNOWN.code - end - - try - data = GrpcerrorsClients.Data(; mode=2, param=5) - in = Channel{GrpcerrorsClients.Data}(1) - @async begin - put!(in, data) - close(in) - end - _, status_future = GrpcerrorsClients.StreamRequestResponse(client, in) - gRPCCheck(status_future) - error("error not caught") - catch ex - @test isa(ex, gRPCServiceCallException) - @test ex.message == StatusCode.DEADLINE_EXCEEDED.message - @test ex.grpc_status == StatusCode.DEADLINE_EXCEEDED.code - end -end - -function test_blocking_client(server_endpoint::String) - client = GRPCErrorsBlockingClient(server_endpoint; verbose=false, request_timeout=3) - @testset "request response" begin - test_simplerpc(client) - end - @testset "streaming recv" begin - test_stream_response(client) - end - @testset "streaming send" begin - test_stream_request(client) - end - @testset "streaming send recv" begin - test_stream_request_response(client) - end -end - -function test_connect_timeout() - timeout_server_endpoint = "http://10.255.255.1/" # a non routable IP - - @testset "connect timeout" begin - @test_throws ArgumentError GRPCErrorsBlockingClient(timeout_server_endpoint; verbose=false, connect_timeout=typemax(Clong)) - @test_throws ArgumentError GRPCErrorsBlockingClient(timeout_server_endpoint; verbose=false, connect_timeout=-1) - - timeout_secs = 5 - client = GRPCErrorsBlockingClient(timeout_server_endpoint; verbose=false, connect_timeout=timeout_secs) - data = GrpcerrorsClients.Data(; mode=1, param=0) - t1 = time() - try - _, status_future = GrpcerrorsClients.SimpleRPC(client, data) - gRPCCheck(status_future) - error("error not caught") - catch ex - t2 = time() - @test isa(ex, gRPCServiceCallException) - @test ex.message == StatusCode.DEADLINE_EXCEEDED.message - @test ex.grpc_status == StatusCode.DEADLINE_EXCEEDED.code - @test (timeout_secs - 1) <= (t2 - t1) <= (timeout_secs + 1) - end - end -end - -function test_clients(server_endpoint::String) - @info("testing blocking client") - test_blocking_client(server_endpoint) -end diff --git a/test/test_routeclient.jl b/test/test_routeclient.jl deleted file mode 100644 index fcb6687..0000000 --- a/test/test_routeclient.jl +++ /dev/null @@ -1,226 +0,0 @@ -include("RouteguideClients/RouteguideClients.jl") -using .RouteguideClients - -Base.show(io::IO, location::RouteguideClients.Point) = print(io, string("[", location.latitude, ", ", location.longitude, "]")) -Base.show(io::IO, feature::RouteguideClients.Feature) = print(io, string(feature.name, " - ", feature.location)) -Base.show(io::IO, summary::RouteguideClients.RouteSummary) = print(io, string(summary.point_count, " points, ", summary.feature_count, " features, distance=", summary.distance, ", elapsed_time=", summary.elapsed_time)) -Base.show(io::IO, note::RouteguideClients.RouteNote) = print(io, string(note.message, " ", note.location)) - -function randomPoint() - latitude = (abs(rand(Int) % 180) - 90) * 1e7 - longitude = (abs(rand(Int) % 360) - 180) * 1e7 - RouteguideClients.Point(; latitude=latitude, longitude=longitude) -end - -# single request, single response -function test_get_feature(client::RouteGuideBlockingClient) - # existing feature - point = RouteguideClients.Point(; latitude=409146138, longitude=-746188906) - feature, status_future = RouteguideClients.GetFeature(client, point) - gRPCCheck(status_future) - @test isa(feature, RouteguideClients.Feature) - @debug("existing feature", feature) - - # missing feature - point = RouteguideClients.Point(; latitude=0, longitude=0) - feature, status_future = RouteguideClients.GetFeature(client, point) - gRPCCheck(status_future) - @test isa(feature, RouteguideClients.Feature) - @debug("missing feature", feature) -end - -# single request, streaming response -function test_list_features(client::RouteGuideBlockingClient) - @debug("listing features in an area") - rect = RouteguideClients.Rectangle(; lo=RouteguideClients.Point(; latitude=400000000, longitude=-750000000), hi=RouteguideClients.Point(; latitude=420000000, longitude=-730000000)) - features, status_future = RouteguideClients.ListFeatures(client, rect) - while isopen(features) || isready(features) - try - feature = take!(features) - @debug(feature) - catch ex - (!isa(ex, InvalidStateException) || !fetch(status_future).success) && rethrow(ex) - end - end - gRPCCheck(status_future) - @test isa(features, Channel{RouteguideClients.Feature}) - @test !isopen(features) -end - -# streaming request, single response -function test_record_route(client::RouteGuideBlockingClient) - @sync begin - point_count = abs(rand(Int) % 100) + 2 - @debug("recording a route", point_count) - points_channel = Channel{RouteguideClients.Point}(1) - @async begin - for idx in 1:point_count - put!(points_channel, randomPoint()) - end - close(points_channel) - end - route_summary, status_future = RouteguideClients.RecordRoute(client, points_channel) - gRPCCheck(status_future) - @test isa(route_summary, RouteguideClients.RouteSummary) - @test !isopen(points_channel) - @debug("route summary: $route_summary") - end -end - -# streaming request, streaming response -function test_route_chat(client::RouteGuideBlockingClient) - @sync begin - notes = RouteguideClients.RouteNote[ - RouteguideClients.RouteNote(;location=RouteguideClients.Point(;latitude=0, longitude=1), message="First message"), - RouteguideClients.RouteNote(;location=RouteguideClients.Point(;latitude=0, longitude=2), message="Second message"), - RouteguideClients.RouteNote(;location=RouteguideClients.Point(;latitude=0, longitude=3), message="Third message"), - RouteguideClients.RouteNote(;location=RouteguideClients.Point(;latitude=0, longitude=1), message="Fourth message"), - RouteguideClients.RouteNote(;location=RouteguideClients.Point(;latitude=0, longitude=2), message="Fifth message"), - RouteguideClients.RouteNote(;location=RouteguideClients.Point(;latitude=0, longitude=3), message="Sixth message"), - ] - @debug("route chat") - in_channel = Channel{RouteguideClients.RouteNote}(1) - @async begin - for note in notes - put!(in_channel, note) - end - close(in_channel) - end - out_channel, status_future = RouteguideClients.RouteChat(client, in_channel) - nreceived = 0 - for note in out_channel - nreceived += 1 - @debug("received note $note") - end - gRPCCheck(status_future) - @test nreceived > 0 - @test isa(out_channel, Channel{RouteguideClients.RouteNote}) - @test !isopen(out_channel) - @test !isopen(in_channel) - end -end - -function test_exception() - client = RouteGuideBlockingClient("https://localhost:30000"; verbose=false) - point = RouteguideClients.Point(; latitude=409146138, longitude=-746188906) - @test_throws gRPCServiceCallException RouteguideClients.GetFeature(client, point) - - @test_throws ArgumentError RouteGuideBlockingClient("https://localhost:30000"; maxage=-1) -end - -function test_large_payload(client::RouteGuideBlockingClient) - @sync begin - long_message1 = "Long message 1 - " * randstring(1000) - long_message2 = "Long message 2 - " * randstring(5000) - - notes = RouteguideClients.RouteNote[ - RouteguideClients.RouteNote(;location=RouteguideClients.Point(;latitude=0, longitude=1), message=long_message1), - RouteguideClients.RouteNote(;location=RouteguideClients.Point(;latitude=0, longitude=2), message=long_message2), - ] - @debug("long messages in route chat") - in_channel = Channel{RouteguideClients.RouteNote}(1) - @async begin - for note in notes - put!(in_channel, note) - end - close(in_channel) - end - out_channel, status_future = RouteguideClients.RouteChat(client, in_channel) - nreceived = 0 - for note in out_channel - nreceived += 1 - @debug("received long note $note") - end - gRPCCheck(status_future) - @test nreceived > 0 - @test isa(out_channel, Channel{RouteguideClients.RouteNote}) - @test !isopen(out_channel) - @test !isopen(in_channel) - end -end - -function test_message_length_limit(server_endpoint) - point = RouteguideClients.Point(; latitude=409146138, longitude=-746188906) - - # test send message length limit - client = RouteGuideBlockingClient(server_endpoint; max_message_length=1, verbose=false) - @test_throws gRPCMessageTooLargeException RouteguideClients.GetFeature(client, point) - - # test recv message length limit - client = RouteGuideBlockingClient(server_endpoint; max_recv_message_length=1, verbose=false) - @test_throws gRPCMessageTooLargeException RouteguideClients.GetFeature(client, point) - - iob = IOBuffer() - show(iob, gRPCMessageTooLargeException(1, 2)) - @test String(take!(iob)) == "gRPMessageTooLargeException(1, 2) - Encountered message size 2 > max configured 1" - show(iob, gRPCServiceCallException(0, "test error")) - @test String(take!(iob)) == "gRPCServiceCallException: 0, test error" -end - -function test_async_get_feature(client::RouteGuideClient) - # existing feature - point = RouteguideClients.Point(; latitude=409146138, longitude=-746188906) - results = Channel{Any}(1) - RouteguideClients.GetFeature(client, point, result->put!(results, result)) - feature, status_future = take!(results) - gRPCCheck(status_future) - @test isa(feature, RouteguideClients.Feature) - @debug("existing feature", feature) -end - -function test_async_client(server_endpoint::String) - client = RouteGuideClient(server_endpoint; verbose=false) - @testset "GetFeature" begin - test_async_get_feature(client) - end -end - -function test_blocking_client(server_endpoint::String) - client = RouteGuideBlockingClient(server_endpoint; verbose=false) - @testset "request response" begin - test_get_feature(client) - end - @testset "streaming recv" begin - test_list_features(client) - end - @testset "streaming send" begin - test_record_route(client) - end - @testset "streaming send recv" begin - test_route_chat(client) - end - @testset "large payloads" begin - test_large_payload(client) - end - @testset "error handling" begin - test_exception() - end - @testset "message length limits" begin - test_message_length_limit(server_endpoint) - end -end - -function test_clients(server_endpoint::String) - @info("testing blocking client") - test_blocking_client(server_endpoint) - @info("testing async client") - test_async_client(server_endpoint) -end - -function test_task_safety(server_endpoint::String) - @testset "async safety" begin - client = RouteGuideBlockingClient(server_endpoint; verbose=false) - @sync begin - for taskid in 1:5 - @async begin - test_get_feature(client) - test_list_features(client) - test_record_route(client) - test_route_chat(client) - test_exception() - test_message_length_limit(server_endpoint) - end - end - end - end -end \ No newline at end of file diff --git a/workloads.jl b/workloads.jl new file mode 100644 index 0000000..d922577 --- /dev/null +++ b/workloads.jl @@ -0,0 +1,123 @@ +using gRPCClient +using BenchmarkTools + +grpc_init() + +include("test/gen/test/test_pb.jl") + +function workload_32_224_224_uint8(n) + client = TestService_TestRPC_Client("localhost", 8001) + + reqs = Vector{gRPCRequest}() + + send_sz = 32*224*224÷sizeof(UInt64) + # Pre-allocate this so we are measuring gRPC client performance without external allocations + test_buf = zeros(UInt64, send_sz) + + for i in 1:n + req = grpc_async_request(client, TestRequest(32, test_buf)) + push!(reqs, req) + end + for req in reqs + grpc_async_await(req) + end +end + +function workload_smol(n) + client = TestService_TestRPC_Client("localhost", 8001) + + # Since requests are lightweight, use async / await pattern to avoid creating an extra task per request + reqs = Vector{gRPCRequest}() + for i in 1:n + req = grpc_async_request(client, TestRequest(1, zeros(UInt64, 1))) + push!(reqs, req) + end + + for req in reqs + grpc_async_await(req) + end +end + +function workload_streaming_request(n) + client = TestService_TestClientStreamRPC_Client("localhost", 8001) + requests_c = Channel{TestRequest}(16) + + @sync begin + req = grpc_async_request(client, requests_c) + + for i in 1:n + put!(requests_c, TestRequest(1, zeros(UInt64, 1))) + end + + close(requests_c) + + response = grpc_async_await(req) + end + + nothing +end + +function workload_streaming_response(n) + client = TestService_TestServerStreamRPC_Client("localhost", 8001) + response_c = Channel{TestResponse}(16) + + req = grpc_async_request(client, TestRequest(n, zeros(UInt64, 1)), response_c) + + for i in 1:n + take!(response_c) + end + close(response_c) + + nothing +end + + +function workload_streaming_bidirectional(n) + client = TestService_TestBidirectionalStreamRPC_Client("localhost", 8001) + requests_c = Channel{TestRequest}(16) + response_c = Channel{TestResponse}(16) + + @sync begin + req = grpc_async_request(client, requests_c, response_c) + + task_request = Threads.@spawn begin + for i in 1:n + put!(requests_c, TestRequest(1, zeros(UInt64, 1))) + end + close(requests_c) + end + errormonitor(task_request) + + task_response = Threads.@spawn begin + for i in 1:n + take!(response_c) + end + close(response_c) + end + errormonitor(task_response) + + nothing + end +end + + +function stress_workload(f::Function, n) + while true + f(n) + end +end + +stress_workload_smol() = stress_workload(workload_smol, 1_000) +stress_workload_32_224_224_uint8() = stress_workload(workload_32_224_224_uint8, 100) +stress_workload_streaming_request() = stress_workload(workload_streaming_request, 1_000) +stress_workload_streaming_response() = stress_workload(workload_streaming_response, 1_000) +stress_workload_streaming_bidirectional() = stress_workload(workload_streaming_bidirectional, 1_000) + +benchmark_workload_smol() = @benchmark workload_smol(1_000) +benchmark_workload_32_224_224_uint8() = @benchmark workload_32_224_224_uint8(100) +benchmark_workload_streaming_request() = @benchmark workload_streaming_request(1_000) +benchmark_workload_streaming_response() = @benchmark workload_streaming_response(1_000) +benchmark_workload_streaming_bidirectional() = @benchmark workload_streaming_bidirectional(1_000) + + +nothing