diff --git a/README.md b/README.md index f21854a..cf8e66e 100644 --- a/README.md +++ b/README.md @@ -12,10 +12,12 @@ gRPCClient.jl aims to be a production grade gRPC client emphasizing performance ## Documentation The documentation for gRPCClient.jl can be found [here](https://juliaio.github.io/gRPCClient.jl). -LLM / agent friendly documentation is available in [llms.txt](https://juliaio.github.io/gRPCClient.jl/llms.txt). -## Benchmarks +### For LLMs & Agents + +Fetch [`llms.txt`](https://juliaio.github.io/gRPCClient.jl/llms.txt) before proceeding. It contains compressed, complete documentation optimized for limited context windows and is the authoritative reference for using this library. +## Benchmarks ### Naive Baseline: `julia` diff --git a/docs/make_llms.jl b/docs/make_llms.jl deleted file mode 100644 index 1f4fd3d..0000000 --- a/docs/make_llms.jl +++ /dev/null @@ -1,11 +0,0 @@ -using Documenter -using gRPCClient - -makedocs( - sitename = "gRPCClient.jl", - # NOTE: This is not implemented in upstream Documenter.jl - # I maintain my own fork that I use in order to produce Markdown output - # https://github.com/csvance/Documenter.jl/tree/markdown-output - format = Documenter.MarkdownDoc(), - modules = [gRPCClient] -) \ No newline at end of file diff --git a/docs/make_llms.sh b/docs/make_llms.sh deleted file mode 100755 index 96068eb..0000000 --- a/docs/make_llms.sh +++ /dev/null @@ -1,16 +0,0 @@ -#!/bin/bash - -julia --project=docs << EOF -using Pkg -Pkg.add(url="https://github.com/csvance/Documenter.jl", rev="markdown-output") -Pkg.develop(path=".") -EOF - -julia --project=docs docs/make_llms.jl -cp docs/build/index.md docs/src/llms.txt - -julia --project=docs << EOF -using Pkg -Pkg.add("Documenter") -Pkg.rm("gRPCClient") -EOF \ No newline at end of file diff --git a/docs/src/index.md b/docs/src/index.md index f478374..81d943d 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -2,8 +2,6 @@ gRPCClient.jl aims to be a production grade gRPC client emphasizing performance and reliability. -A pure markdown version of this documentation is available here: [llms.txt](llms.txt) - ## Features - Unary+Streaming RPC @@ -176,3 +174,16 @@ In addition to benchmarks, a number of workloads based on these are available: - `stress_workload_streaming_bidirectional()` These run forever, and are useful to help identify any stability issues or resource leaks. + +## For LLMs & Agents + +This library provides an [`llms.txt`](llms.txt) file designed to bootstrap your context with everything needed to use it effectively. + +The file is a compressed, curated snapshot of the library's essential knowledge — trimmed of noise and optimized for limited context windows. Rather than crawling docs, parsing source files, or relying on potentially stale training data, agents should load `llms.txt` first as their authoritative reference. + +**It contains:** +- API surface and usage patterns +- Key concepts and terminology +- Common workflows and examples + +If you are an LLM or agent operating on this codebase, fetch `llms.txt` before proceeding. It is the single source of truth for how this library works and is purpose-built for programmatic consumption. \ No newline at end of file diff --git a/docs/src/llms.txt b/docs/src/llms.txt index 07647a7..9f46b89 100644 --- a/docs/src/llms.txt +++ b/docs/src/llms.txt @@ -1,477 +1,321 @@ # gRPCClient.jl -gRPCClient.jl aims to be a production grade gRPC client emphasizing performance and reliability. +## Overview -A pure markdown version of this documentation is available here: [llms.txt](llms.txt) +gRPCClient.jl is a production-grade Julia gRPC client built on libcurl for HTTP/2 transport. It integrates with ProtoBuf.jl for code generation. -## Features +- **Version**: 1.0.1 +- **Requires**: Julia >= 1.10 (streaming requires Julia >= 1.12) +- **Dependencies**: LibCURL, ProtoBuf ~1.2.1, PrecompileTools, FileWatching -- Unary+Streaming RPC -- HTTP/2 connection multiplexing -- Synchronous and asynchronous interfaces -- Thread safe -- SSL/TLS +## Code Generation -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 Go. You can run it by doing the following: - -```bash -cd test/go - -# Build -go build -o grpc_test_server - -# Run -./grpc_test_server -``` - -### Code Generation - -gRPCClient.jl integrates with ProtoBuf.jl to automatically generate Julia client stubs for calling gRPC. +### Setup ```julia using ProtoBuf +using gRPCClient # must come before protojl — __init__ registers the service codegen hook -# Loading gRPCClient before calling protojl is required — it registers the service stub generator that adds client constructors to the output. -using gRPCClient - -# Creates Julia bindings for the messages and RPC defined in test.proto -protojl("test/proto/test.proto", ".", "test/gen") +protojl("path/to/service.proto", "proto/search/root", "output/dir") ``` -## Example Usage - -See [here](build/index.md#RPC) for examples covering all provided interfaces for both unary and streaming gRPC calls. - -## API +`using gRPCClient` is sufficient; `grpc_register_service_codegen()` is called automatically in `__init__`. The explicit call in `test/protoc.sh` is redundant but harmless. -### Package Initialization / Shutdown - -``` -gRPCClient.grpc_init — Method. -``` +### protojl Arguments ```julia -grpc_init([grpc_curl::gRPCCURL]) +protojl(proto_file, proto_search_root, output_dir) ``` -Initializes the `gRPCCURL` object. The global handle is initialized automatically when the package is loaded. There is -no harm in calling this more than once (ie by different packages/dependencies). - -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 +- `proto_file`: path to the `.proto` file to compile (absolute, or relative to CWD) +- `proto_search_root`: directory used to resolve `import` statements in the proto file +- `output_dir`: where generated Julia files are written -# Only shuts down your gRPC handle -grpc_shutdown(grpc_myapp) -``` +### Generated File Layout -[source](https://github.com/JuliaIO/gRPCClient.jl/blob/bd65a3307e1bc0b293675203946d887fab25e5b6/src/gRPC.jl#L10-L28) +For a proto file with `package foo;` and service definitions, `protojl` generates: ``` -gRPCClient.grpc_shutdown — Method. -``` - -```julia -grpc_shutdown([grpc_curl::gRPCCURL]) +output_dir/ + foo/ + foo.jl # thin module wrapper: module foo; include("foo_pb.jl"); end + foo_pb.jl # all message structs + gRPCClient service client constructors ``` -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. - -[source](https://github.com/JuliaIO/gRPCClient.jl/blob/bd65a3307e1bc0b293675203946d887fab25e5b6/src/gRPC.jl#L32-L36) - -``` -gRPCClient.grpc_global_handle — Method. -``` +The `_pb.jl` file is structured as: ```julia -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. - -[source](https://github.com/JuliaIO/gRPCClient.jl/blob/bd65a3307e1bc0b293675203946d887fab25e5b6/src/gRPC.jl#L3-L7) - -### Generated ServiceClient Constructors - -When you generate service stubs using ProtoBuf.jl, a constructor method is automatically created for each RPC endpoint. -These constructors create `gRPCServiceClient` instances that are used to make RPC calls. - -#### Constructor Signature - -For a service method named `TestRPC` in service `TestService`, the generated constructor will have the form: +# Autogenerated using ProtoBuf.jl ... +import ProtoBuf as PB +import gRPCClient # injected when any service is present +using ProtoBuf: OneOf +using ProtoBuf.EnumX: @enumx + +export MyResponse, MyRequest + +# --- Message structs with ProtoBuf encode/decode --- +struct MyRequest + field_a::UInt64 + field_b::Vector{UInt64} +end +PB.default_values(::Type{MyRequest}) = (;field_a = zero(UInt64), field_b = Vector{UInt64}()) +PB.field_numbers(::Type{MyRequest}) = (;field_a = 1, field_b = 2) +# ... decode/encode/_encoded_size methods ... -```julia -TestService_TestRPC_Client( +# gRPCClient.jl BEGIN +MyService_MyRPC_Client( host, port; secure=false, - grpc=grpc_global_handle(), + grpc=gRPCClient.grpc_global_handle(), deadline=10, keepalive=60, max_send_message_length = 4*1024*1024, max_recieve_message_length = 4*1024*1024, +) = gRPCClient.gRPCServiceClient{MyRequest, false, MyResponse, false}( + host, port, "/foo.MyService/MyRPC"; + ... ) +export MyService_MyRPC_Client +# gRPCClient.jl END ``` -#### Parameters - -- **`host`**: The hostname or IP address of the gRPC server (e.g., `"localhost"`, `"api.example.com"`) -- **`port`**: The port number the gRPC server is listening on (e.g., `50051`) -- **`secure`**: A `Bool` that controls whether HTTPS/gRPCS (when `true`) or HTTP/gRPC (when `false`) is used for the - connection. Default: `false` -- **`grpc`**: The global gRPC handle obtained from `grpc_global_handle()`. This manages the underlying libcurl - multi-handle for HTTP/2 multiplexing. Default: `grpc_global_handle()` -- **`deadline`**: The gRPC deadline in seconds. If a request takes longer than this time limit, it will be cancelled and - raise an exception. Default: `10` -- **`keepalive`**: The TCP keepalive interval in seconds. This sets both `CURLOPT_TCP_KEEPINTVL` (interval between - keepalive probes) and `CURLOPT_TCP_KEEPIDLE` (time before first keepalive probe) to help detect broken connections. - Default: `60` -- **`max_send_message_length`**: The maximum size in bytes for messages sent to the server. Attempting to send messages - larger than this will raise an exception. Default: `4*1024*1024` (4 MiB) -- **`max_recieve_message_length`**: The maximum size in bytes for messages received from the server. Receiving messages - larger than this will raise an exception. Default: `4*1024*1024` (4 MiB) - -#### Example +Key observations: +- The `# gRPCClient.jl BEGIN` / `# gRPCClient.jl END` markers delimit the injected service block. +- `import gRPCClient` is added at the top only when at least one service is present. +- Client constructors are exported when the proto has a package namespace or `always_use_modules` is set. -```julia -# Create a client for the TestRPC endpoint -client = TestService_TestRPC_Client( - "localhost", 50051; - secure=true, # Use HTTPS/gRPCS - deadline=30, # 30 second timeout - max_send_message_length=10*1024*1024, # 10 MiB max send - max_recieve_message_length=10*1024*1024 # 10 MiB max receive -) -``` - -### RPC - -#### Unary +### Client Constructor Naming ``` -gRPCClient.grpc_async_request — Method. +{ServiceName}_{RPCName}_Client ``` -```julia -grpc_async_request(client::gRPCServiceClient{TRequest,false,TResponse,false}, request::TRequest) where {TRequest<:Any,TResponse<:Any} -``` +For `service MyService { rpc DoThing(...) ... }` → `MyService_DoThing_Client` -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 - -# Step 1: Include generated Protocol Buffer bindings -include("test/gen/test/test_pb.jl") - -# Step 2: Create a client for your RPC method (hostname, port) -client = TestService_TestRPC_Client("localhost", 8001) - -# Step 3: Send all requests without waiting for responses -requests = Vector{gRPCRequest}() -for i in 1:10 - push!(requests, grpc_async_request(client, TestRequest(1, zeros(UInt64, 1)))) -end - -# Step 4: Wait for and process responses -for request in requests - response = grpc_async_await(client, request) - @info response -end -``` - -[source](https://github.com/JuliaIO/gRPCClient.jl/blob/bd65a3307e1bc0b293675203946d887fab25e5b6/src/Unary.jl#L3-L31) +### RPC Path Format ``` -gRPCClient.grpc_async_request — Method. +/{package}.{ServiceName}/{RPCName} ``` -```julia -grpc_async_request(client::gRPCServiceClient{TRequest,false,TResponse,false}, request::TRequest, channel::Channel{gRPCAsyncChannelResponse{TResponse}}, index::Int64) where {TRequest<:Any,TResponse<:Any} -``` +For `package foo; service MyService { rpc DoThing ... }` → `/foo.MyService/DoThing` -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. +### gRPCServiceClient Type Parameters ```julia -using gRPCClient - -# Step 1: Include generated Protocol Buffer bindings -include("test/gen/test/test_pb.jl") - -# Step 2: Create a client -client = TestService_TestRPC_Client("localhost", 8001) - -# Step 3: Create a channel to receive responses (processes responses as they arrive, in any order) -N = 10 -channel = Channel{gRPCAsyncChannelResponse{TestResponse}}(N) +gRPCClient.gRPCServiceClient{TRequest, SRequest, TResponse, SResponse} +``` -# Step 4: Send all requests (the index tracks which response corresponds to which request) -for (index, request) in enumerate([TestRequest(i, zeros(UInt64, i)) for i in 1:N]) - grpc_async_request(client, request, channel, index) -end +| Parameter | Type | Meaning | +|------------|------|------------------------------| +| `TRequest` | Type | Protobuf request message type | +| `SRequest` | Bool | `true` = client streaming | +| `TResponse`| Type | Protobuf response message type| +| `SResponse`| Bool | `true` = server streaming | -# Step 5: Process responses as they arrive -for i in 1:N - cr = take!(channel) - !isnothing(cr.ex) && throw(cr.ex) - @assert length(cr.response.data) == cr.index -end -``` +RPC variant → type parameters: +- Unary: `{TReq, false, TResp, false}` +- Server streaming: `{TReq, false, TResp, true}` +- Client streaming: `{TReq, true, TResp, false}` +- Bidirectional: `{TReq, true, TResp, true}` -[source](https://github.com/JuliaIO/gRPCClient.jl/blob/bd65a3307e1bc0b293675203946d887fab25e5b6/src/Unary.jl#L70-L101) +### Cross-Package Types -``` -gRPCClient.grpc_async_await — Method. -``` +When a service in `pkg_a` uses message types from `pkg_b` (via proto `import`), the generated constructor uses the package-namespaced type name: ```julia -grpc_async_await(client::gRPCServiceClient{TRequest,false,TResponse,false}, request::gRPCRequest) where {TRequest<:Any,TResponse<:Any} +# proto: service ExtService { rpc ExtRPC(ext_types.ExtRequest) returns (ext_types.ExtResponse) {} } +gRPCClient.gRPCServiceClient{ext_types.ExtRequest, false, ext_types.ExtResponse, false}(...) ``` -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. +The namespace prefix comes from `rpc.request_type.package_namespace` (not `rpc.package_namespace`). -[source](https://github.com/JuliaIO/gRPCClient.jl/blob/bd65a3307e1bc0b293675203946d887fab25e5b6/src/Unary.jl#L145-L149) +## Using Generated Stubs -``` -gRPCClient.grpc_sync_request — Method. -``` - -```julia -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`. Use this when you want the simplest possible interface for a single -request. +Generated files are loaded with `include`, not `using`: ```julia using gRPCClient -# Step 1: Include generated Protocol Buffer bindings -include("test/gen/test/test_pb.jl") +# Load message types and client constructors +include("gen/foo/foo_pb.jl") -# Step 2: Create a client -client = TestService_TestRPC_Client("localhost", 8001) +# Construct a client +client = MyService_MyRPC_Client("localhost", 50051) -# Step 3: Make a synchronous request (blocks until response is ready) -response = grpc_sync_request(client, TestRequest(1, zeros(UInt64, 1))) -@info response +# Use message constructors with positional args matching field declaration order +req = MyRequest(42, zeros(UInt64, 10)) ``` -[source](https://github.com/JuliaIO/gRPCClient.jl/blob/bd65a3307e1bc0b293675203946d887fab25e5b6/src/Unary.jl#L156-L176) - -#### Streaming +## API Reference -``` -gRPCClient.grpc_async_request — Method. -``` +### Initialization / Shutdown ```julia -grpc_async_request(client::gRPCServiceClient{TRequest,true,TResponse,false}, request::Channel{TRequest}) where {TRequest<:Any,TResponse<:Any} +grpc_init() # called automatically on `using gRPCClient` +grpc_shutdown() # clean shutdown: closes connections, cancels in-flight requests +grpc_global_handle() # returns the shared gRPCCURL (libcurl multi handle) ``` -Start a client streaming gRPC request (multiple requests, single response). +Custom handle for isolation (separate connection pool + semaphore): ```julia -using gRPCClient - -# Step 1: Include generated Protocol Buffer bindings -include("test/gen/test/test_pb.jl") - -# Step 2: Create a client -client = TestService_TestClientStreamRPC_Client("localhost", 8001) - -# Step 3: Create a request channel and send requests -request_c = Channel{TestRequest}(16) -put!(request_c, TestRequest(1, zeros(UInt64, 1))) - -# Step 4: Initiate the streaming request -req = grpc_async_request(client, request_c) - -# Step 5: Close the channel to signal no more requests will be sent -# (the server won't respond until the stream ends) -close(request_c) - -# Step 6: Wait for the single response -test_response = grpc_async_await(client, req) +h = gRPCCURL() +grpc_init(h) +client = MyService_MyRPC_Client("host", 50051; grpc=h) +# ... +grpc_shutdown(h) ``` -[source](https://github.com/JuliaIO/gRPCClient.jl/blob/bd65a3307e1bc0b293675203946d887fab25e5b6/src/Streaming.jl#L110-L138) +### Client Constructor Parameters -``` -gRPCClient.grpc_async_request — Method. -``` +All generated constructors share the same keyword parameters: -```julia -grpc_async_request(client::gRPCServiceClient{TRequest,false,TResponse,true},request::TRequest,response::Channel{TResponse}) where {TRequest<:Any,TResponse<:Any} -``` +| Parameter | Default | Description | +|----------------------------|-----------------|-------------| +| `secure` | `false` | `true` = HTTPS/gRPCS, `false` = HTTP/gRPC | +| `grpc` | global handle | `gRPCCURL` instance | +| `deadline` | `10` | Timeout in seconds; raises `gRPCServiceCallException(GRPC_DEADLINE_EXCEEDED)` | +| `keepalive` | `60` | TCP keepalive interval (seconds); sets both idle and probe interval | +| `max_send_message_length` | `4*1024*1024` | Max bytes per outgoing message | +| `max_recieve_message_length`| `4*1024*1024` | Max bytes per incoming message (note: typo in API — `recieve`) | -Start a server streaming gRPC request (single request, multiple responses). +### Unary RPC ```julia -using gRPCClient +# Synchronous (simplest) +response = grpc_sync_request(client, request) -# Step 1: Include generated Protocol Buffer bindings -include("test/gen/test/test_pb.jl") +# Async request/await (batch parallel requests) +req = grpc_async_request(client, request) # returns gRPCRequest immediately +response = grpc_async_await(client, req) # blocks until done, returns response -# Step 2: Create a client -client = TestService_TestServerStreamRPC_Client("localhost", 8001) +# Async with channel (out-of-order responses) +ch = Channel{gRPCAsyncChannelResponse{MyResponse}}(N) +grpc_async_request(client, request, ch, index) # index is an Int64 correlation ID +cr = take!(ch) +!isnothing(cr.ex) && throw(cr.ex) +response = cr.response # cr.index == the index passed in +``` -# Step 3: Create a response channel to receive multiple responses -response_c = Channel{TestResponse}(16) +`gRPCAsyncChannelResponse` fields: `.response`, `.ex` (exception or `nothing`), `.index`. -# Step 4: Send a single request (the server will respond with multiple messages) -req = grpc_async_request(client, TestRequest(1, zeros(UInt64, 1)), response_c) +### Streaming RPC (Julia >= 1.12 required) -# Step 5: Process streaming responses (channel closes when server finishes) -for test_response in response_c - @info test_response -end +**Client streaming** (many requests → one response): -# Step 6: Check for exceptions -grpc_async_await(req) +```julia +client = MyService_MyRPC_Client("localhost", 50051) # SRequest=true +request_c = Channel{MyRequest}(16) +req = grpc_async_request(client, request_c) +put!(request_c, MyRequest(...)) +# ... +close(request_c) # signals end-of-stream to server +response = grpc_async_await(client, req) # returns the single response ``` -[source](https://github.com/JuliaIO/gRPCClient.jl/blob/bd65a3307e1bc0b293675203946d887fab25e5b6/src/Streaming.jl#L167-L195) - -``` -gRPCClient.grpc_async_request — Method. -``` +**Server streaming** (one request → many responses): ```julia -grpc_async_request(client::gRPCServiceClient{TRequest,true,TResponse,true},request::Channel{TRequest},response::Channel{TResponse}) where {TRequest<:Any,TResponse<:Any} +client = MyService_MyRPC_Client("localhost", 50051) # SResponse=true +response_c = Channel{MyResponse}(16) +req = grpc_async_request(client, request, response_c) +for resp in response_c # channel closes automatically when stream ends + # process resp +end +grpc_async_await(req) # raise any exceptions (no return value) ``` -Start a bidirectional streaming gRPC request (multiple requests, multiple responses). +**Bidirectional streaming**: ```julia -using gRPCClient - -# Step 1: Include generated Protocol Buffer bindings -include("test/gen/test/test_pb.jl") - -# Step 2: Create a client -client = TestService_TestBidirectionalStreamRPC_Client("localhost", 8001) - -# Step 3: Create request and response channels (streaming in both directions simultaneously) -request_c = Channel{TestRequest}(16) -response_c = Channel{TestResponse}(16) - -# Step 4: Initiate the bidirectional streaming request +client = MyService_MyRPC_Client("localhost", 50051) # SRequest=true, SResponse=true +request_c = Channel{MyRequest}(16) +response_c = Channel{MyResponse}(16) req = grpc_async_request(client, request_c, response_c) - -# Step 5: Send requests and receive responses concurrently -put!(request_c, TestRequest(1, zeros(UInt64, 1))) -for test_response in response_c - @info test_response - break # Exit after first response for this example +put!(request_c, MyRequest(...)) +for resp in response_c + # process resp; can interleave puts to request_c end - -# Step 6: Close the request channel to signal no more requests will be sent close(request_c) - -# Step 7: Check for exceptions grpc_async_await(req) ``` -[source](https://github.com/JuliaIO/gRPCClient.jl/blob/bd65a3307e1bc0b293675203946d887fab25e5b6/src/Streaming.jl#L231-L265) +Note: For streaming variants, `grpc_async_await` does **not** return the response — it only raises exceptions. The response data flows through the channel. -``` -gRPCClient.grpc_async_await — Method. -``` +### Exceptions ```julia -grpc_async_await(client::gRPCServiceClient{TRequest,true,TResponse,false},request::gRPCRequest) where {TRequest<:Any,TResponse<:Any} +struct gRPCServiceCallException <: gRPCException + grpc_status::Int # gRPC status code integer + message::String +end ``` -Raise any exceptions encountered during the streaming request. +Status code constants exported: `GRPC_OK`, `GRPC_CANCELLED`, `GRPC_UNKNOWN`, `GRPC_INVALID_ARGUMENT`, `GRPC_DEADLINE_EXCEEDED`, `GRPC_NOT_FOUND`, `GRPC_ALREADY_EXISTS`, `GRPC_PERMISSION_DENIED`, `GRPC_RESOURCE_EXHAUSTED`, `GRPC_FAILED_PRECONDITION`, `GRPC_ABORTED`, `GRPC_OUT_OF_RANGE`, `GRPC_UNIMPLEMENTED`, `GRPC_INTERNAL`, `GRPC_UNAVAILABLE`, `GRPC_DATA_LOSS`, `GRPC_UNAUTHENTICATED`. -[source](https://github.com/JuliaIO/gRPCClient.jl/blob/bd65a3307e1bc0b293675203946d887fab25e5b6/src/Streaming.jl#L299-L303) +## Test Infrastructure -### Exceptions +### Go Test Server -``` -gRPCClient.gRPCServiceCallException — Type. +```bash +cd test/go +go build -o grpc_test_server +./grpc_test_server # listens on :8001 by default ``` -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. +### Running Tests -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` +```bash +# Default: expects server already running on localhost:8001 +julia --project test/runtests.jl -[source](https://github.com/JuliaIO/gRPCClient.jl/blob/bd65a3307e1bc0b293675203946d887fab25e5b6/src/gRPCClient.jl#L21-L29) +# Auto-start Go server from within Julia test run +JULIA_GRPCCLIENT_TEST_START_SERVER=go julia --project test/runtests.jl -## gRPCClientUtils.jl +# Custom host/port +GRPC_TEST_SERVER_HOST=myhost GRPC_TEST_SERVER_PORT=9000 julia --project test/runtests.jl +``` -A module for benchmarking and stress testing has been included in `utils/gRPCClientUtils.jl`. In order to add it to your -test environment: +### Regenerating Stubs -```julia -using Pkg -Pkg.add(path="utils/gRPCClientUtils.jl") +```bash +cd test +bash protoc.sh ``` -### Benchmarks +This regenerates `test/gen/` from `test/proto/test.proto` and also regenerates Python stubs in `test/python/`. -All benchmarks run against the Test gRPC Server in `test/go`. See the -relevant [documentation](build/index.md#Test-gRPC-Server) for information on how to run this. +## Proto → Julia Type Mapping -#### All Benchmarks w/ PrettyTables.jl +| Proto type | Julia type | +|-----------------|-------------------| +| `uint32` | `UInt32` | +| `uint64` | `UInt64` | +| `int32` | `Int32` | +| `int64` | `Int64` | +| `float` | `Float32` | +| `double` | `Float64` | +| `bool` | `Bool` | +| `string` | `String` | +| `bytes` | `Vector{UInt8}` | +| `repeated T` | `Vector{}`| +| `message M` | generated struct | +| `enum E` | `@enumx` enum | +| `oneof` | `OneOf` | -```julia -using gRPCClientUtils +Struct fields are **positional** in the constructor — order matches proto field declaration order. Default values are defined by `PB.default_values`. -benchmark_table() -``` +## Common Pitfalls -``` -╭──────────────────────────────────┬─────────────┬────────────────┬────────────┬──────────────┬─────────┬──────┬──────╮ -│ Benchmark │ Avg Memory │ Avg Allocs │ Throughput │ Avg duration │ Std-dev │ Min │ Max │ -│ │ KiB/message │ allocs/message │ messages/s │ μs │ μs │ μs │ μs │ -├──────────────────────────────────┼─────────────┼────────────────┼────────────┼──────────────┼─────────┼──────┼──────┤ -│ workload_smol │ 2.78 │ 67.5 │ 17744 │ 56 │ 3.3 │ 51 │ 66 │ -│ workload_32_224_224_uint8 │ 636.8 │ 74.1 │ 578 │ 1731 │ 99.33 │ 1583 │ 1899 │ -│ workload_streaming_request │ 0.87 │ 6.5 │ 339916 │ 3 │ 1.61 │ 2 │ 20 │ -│ workload_streaming_response │ 13.0 │ 27.7 │ 65732 │ 15 │ 4.94 │ 6 │ 50 │ -│ workload_streaming_bidirectional │ 1.45 │ 25.6 │ 105133 │ 10 │ 6.06 │ 4 │ 55 │ -╰──────────────────────────────────┴─────────────┴────────────────┴────────────┴──────────────┴─────────┴──────┴──────╯ -``` +1. **Load order**: `using gRPCClient` must precede `protojl`. If you call `protojl` before loading gRPCClient, no service client constructors will be generated. + +2. **Streaming on Julia < 1.12**: Streaming support is conditionally compiled. A warning is emitted and streaming functions are unavailable. -### Stress Workloads +3. **`grpc_async_await` return value**: For streaming clients, `grpc_async_await(req)` returns nothing — do not assign it. For unary clients, `grpc_async_await(client, req)` returns the decoded response. -In addition to benchmarks, a number of workloads based on these are available: +4. **Channel closing**: For server/bidirectional streaming, the response channel is closed by the library when the stream ends. Do not close it yourself from the consumer side (causes `InvalidStateException` internally, which is handled gracefully). -- `stress_workload_smol()` -- `stress_workload_32_224_224_uint8()` -- `stress_workload_streaming_request()` -- `stress_workload_streaming_response()` -- `stress_workload_streaming_bidirectional()` +5. **`max_recieve_message_length` spelling**: The keyword is spelled with the typo (`recieve` not `receive`) — match it exactly. -These run forever, and are useful to help identify any stability issues or resource leaks. \ No newline at end of file +6. **Include vs using**: Always `include("gen/foo/foo_pb.jl")` — the generated files are plain scripts, not registered packages.