diff --git a/.gitignore b/.gitignore index 2503dc2..57d40d9 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ target/ Cargo.lock tags commit-changes.sh +context-snapshot.yaml diff --git a/ARENA_THREADING_NOTES.md b/ARENA_THREADING_NOTES.md deleted file mode 100644 index 215011c..0000000 --- a/ARENA_THREADING_NOTES.md +++ /dev/null @@ -1,238 +0,0 @@ -# Arena Threading Implementation Notes - -## Current Architecture - -The decode path goes: -``` -Streaming - └─> decode_chunk() [tocin/src/codec/decode.rs:375] - └─> decoder.decode(&mut decode_buf) - └─> ProstDecoder::decode() [tocin-defiant/src/codec.rs:130] - └─> Arena::new() ← PROBLEM: Creates arena here, drops immediately -``` - -## Problem - -Line `tocin-defiant/src/codec.rs:130-141`: -```rust -fn decode(&mut self, buf: &mut DecodeBuf<'_>) -> Result, Self::Error> { - let arena = Arena::new(); // Created per decode - let item = Decode::decode(buf, &arena)?; - Ok(item) // arena dropped, item uses owned String -} -``` - -The arena is created and dropped inside the `decode` method, so decoded messages can't actually borrow from it - they must use owned `String` types. - -## Required Changes - -### 1. Update `Decoder` Trait - -**File:** `tocin/src/codec/mod.rs:142-160` - -**Current:** -```rust -pub trait Decoder { - type Item; - type Error: From; - - fn decode(&mut self, src: &mut DecodeBuf<'_>) -> Result, Self::Error>; -} -``` - -**Target:** -```rust -pub trait Decoder { - type Item<'a> where Self: 'a; // GAT for arena lifetime - type Error: From; - - fn decode<'a>( - &mut self, - src: &mut DecodeBuf<'_>, - arena: &'a defiant::Arena, - ) -> Result>, Self::Error>; -} -``` - -**Issues:** -- This requires GATs (Generic Associated Types) - stable since Rust 1.65 -- Breaking change to `Decoder` trait -- All implementors must update - -### 2. Thread Arena Through `Streaming` - -**File:** `tocin/src/codec/decode.rs` - -**Current struct:** -```rust -pub struct Streaming { - decoder: SyncWrapper + Send + 'static>>, - inner: StreamingInner, -} -``` - -**Target struct:** -```rust -pub struct Streaming { - decoder: SyncWrapper Decoder = T, Error = Status> + Send + 'static>>, - inner: StreamingInner, - arena: defiant::Arena, // Connection-owned arena -} -``` - -**Decode method change (line 375):** -```rust -// Before -match self.decoder.get_mut().decode(&mut decode_buf)? { - -// After -match self.decoder.get_mut().decode(&mut decode_buf, &self.arena)? { -``` - -### 3. Update `ProstDecoder` Implementation - -**File:** `tocin-defiant/src/codec.rs:126-146` - -**Target:** -```rust -impl Decode<'a> + Default> Decoder for ProstDecoder { - type Item<'arena> = U where Self: 'arena; - type Error = Status; - - fn decode<'a>( - &mut self, - buf: &mut DecodeBuf<'_>, - arena: &'a Arena, // Passed from Streaming - ) -> Result>, Self::Error> { - // Use provided arena - let item = Decode::decode(buf, arena) - .map(Option::Some) - .map_err(from_decode_error)?; - - Ok(item) // item contains &'a str - } -} -``` - -### 4. Update `ProstCodec` Type Bounds - -**File:** `tocin-defiant/src/codec.rs:50-74` - -**Current:** -```rust -impl Codec for ProstCodec -where - T: Encode + Send + 'static, - U: for<'a> Decode<'a> + Default + Send + 'static, -{ - type Encode = T; - type Decode = U; // ← This becomes problematic with GATs - ... -} -``` - -**Challenge:** The `Codec` trait also needs updating to support GATs. - -### 5. Update `Codec` Trait - -**File:** `tocin/src/codec/mod.rs:105-120` - -**Current:** -```rust -pub trait Codec { - type Encode: Send + 'static; - type Decode: Send + 'static; - - type Encoder: Encoder + Send + 'static; - type Decoder: Decoder + Send + 'static; - ... -} -``` - -**Target:** -```rust -pub trait Codec { - type Encode: Send + 'static; - type Decode<'a>: Send + 'a; // GAT - - type Encoder: Encoder + Send + 'static; - type Decoder: for<'a> Decoder = Self::Decode<'a>, Error = Status> + Send + 'static; - ... -} -``` - -## Monoio-Specific Simplification - -**Key insight:** Once we move to monoio, we can remove `Send` bounds since tasks never migrate threads! - -**Simplified traits for monoio:** -```rust -pub trait Decoder { - type Item<'a> where Self: 'a; - type Error: From; - - fn decode<'a>( - &mut self, - src: &mut DecodeBuf<'_>, - arena: &'a defiant::Arena, - ) -> Result>, Self::Error>; -} - -// No Send required! -pub trait Codec { - type Encode: 'static; - type Decode<'a>: 'a; - - type Encoder: Encoder + 'static; - type Decoder: for<'a> Decoder = Self::Decode<'a>, Error = Status> + 'static; - ... -} -``` - -## Alternative Approach: Skip Intermediate Step - -Given the complexity, we might want to: - -1. **Skip** updating current tokio-based tocin with arenas -2. **Go directly** to monoio implementation with arena support from the start -3. Rewrite the transport layer first, then update codec traits - -This avoids: -- Complex GAT threading through tokio-based code -- `Send` bound complications -- Maintaining two versions (tokio + monoio) - -## Recommended Path Forward - -### Option A: Incremental (Complex) -1. Add GATs to Decoder/Codec traits -2. Thread arena through Streaming -3. Update ProstDecoder -4. Test with tokio (still creating per-decode arenas) -5. Then switch to monoio - -### Option B: Direct (Simpler) -1. Create new `tocin::monoio` module -2. Implement monoio transport with arena-aware Decoder trait from scratch -3. Copy/adapt server/client logic with new trait definitions -4. Remove tokio dependencies -5. Delete old transport code - -**Recommendation: Option B** - -Reasoning: -- Fewer intermediate states -- Cleaner final architecture -- Avoid complex GAT + Send bound interactions -- Can test monoio integration independently - -## Next Steps - -If going with Option B: -1. Create `tocin/src/monoio/` module -2. Define arena-aware Decoder/Codec traits in that module -3. Implement monoio-http based transport -4. Update tocin-defiant to implement new traits -5. Update examples to use monoio - -This keeps the old tokio code intact during development and makes the transition cleaner. diff --git a/Cargo.toml b/Cargo.toml index 1d4a206..c1e2f88 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,16 +2,16 @@ members = [ "tocin", "tocin-build", - # "tocin-health", # Depends on InterceptedService, not yet updated + "tocin-health", # Re-enabling to test arena lifetime handling # "tocin-protobuf", # Only needed for Google protobuf interop, not core tocin # "tocin-protobuf-build", "tocin-types", - # "tocin-reflection", # Depends on tower Service, not yet updated + "tocin-reflection", "tocin-defiant", "tocin-defiant-build", - # "tocin-web", # Depends on Body::new, not yet updated + # "tocin-web", # Removed: not appropriate for perf-oriented thread-per-core servers # Temporarily disabled during monoio migration: - # "examples", + "examples", # "codegen", # "grpc", # "interop", @@ -40,6 +40,7 @@ monoio-rustls = { version = "0.1" } # Arena-based protobuf defiant = { path = "../defiant/defiant" } +defiant-types = { path = "../defiant/defiant-types" } # Common dependencies bytes = "1.0" diff --git a/INTEGRATION_PLAN.md b/INTEGRATION_PLAN.md deleted file mode 100644 index fb50e3c..0000000 --- a/INTEGRATION_PLAN.md +++ /dev/null @@ -1,797 +0,0 @@ -# Tocin + Monoio + Defiant Integration Plan - -## Overview - -This document describes the integration of three components for high-performance gRPC: - -1. **Tocin** (renamed tonic fork): gRPC framework -2. **Monoio**: Thread-per-core async runtime (io_uring based) -3. **Defiant** (renamed prost fork): Arena-based protobuf with zero-copy deserialization - -**Repository Links:** -- Tocin: `/home/dan/Development/en/memory/tocin/` (local) -- Monoio: `/home/dan/Development/en/memory/monoio/` (local cache) -- Monoio-http: `/home/dan/Development/en/memory/monoio-http/` (local cache) -- Defiant: `/home/dan/Development/en/memory/defiant/` (local) + `github.com/dwerner/defiant` - ---- - -## Why This Combination? - -### Problem with Original Tonic + Prost -- Tokio's work-stealing scheduler moves tasks between threads at `.await` points -- Arena-allocated messages contain `&'arena` references (not `Send`) -- Messages become dangling pointers if task migrates threads -- **Conclusion**: Arena allocation fundamentally incompatible with tokio - -### Solution: Thread-Per-Core -- Monoio tasks NEVER migrate threads (pinned to core) -- Tasks can be `!Send` (no cross-thread requirement) -- Arena-allocated messages with `&'arena str` work perfectly -- Each connection owns an arena, resets between requests - ---- - -## Architecture - -### Current Tonic Stack (Tokio-based) -``` -┌─────────────────────┐ -│ Tonic (gRPC) │ -├─────────────────────┤ -│ Tower (Service) │ -├─────────────────────┤ -│ Hyper (HTTP/2) │ -├─────────────────────┤ -│ Tokio (Runtime) │ ← Work-stealing (tasks migrate threads) -└─────────────────────┘ -``` - -### Target Tocin Stack (Monoio-based) -``` -┌─────────────────────┐ -│ Tocin (gRPC) │ -├─────────────────────┤ -│ Simple async fns │ ← No tower, direct composition -├─────────────────────┤ -│ Monoio-http (H2) │ ← Native HTTP/2 implementation -├─────────────────────┤ -│ Monoio (Runtime) │ ← Thread-per-core (tasks pinned) -└─────────────────────┘ -``` - ---- - -## Defiant: Arena-Based Protobuf - -### Key Concepts - -**Arena Allocation:** -```rust -let mut arena = Arena::with_capacity(64 * 1024); - -// Decode: copies wire bytes into arena once -let message = HelloRequest::decode(bytes, &arena)?; - -// message.name() returns &'arena str (zero-copy read) -println!("{}", message.name()); - -// Reset for next message (keeps capacity) -arena.reset(); -``` - -**View/Builder Pattern:** -```rust -// Reading (Views - immutable, borrowed from arena) -let view: HelloRequest<'arena> = decode(bytes, &arena)?; -let name: &str = view.name(); // &'arena str - -// Writing (Builders - mutable, allocate into arena) -let mut builder = HelloRequestBuilder::new(&arena); -builder.set_name("Alice"); -builder.set_age(30); -let view: HelloRequest<'arena> = builder.build(); -``` - -### Performance Characteristics - -**Benchmark Results (from defiant):** -``` -Small messages (12 bytes): 237-253ns (identical to owned) -Medium messages (1KB): ~400ns (37% faster than owned) -Large messages (84KB): 105µs (52% faster than owned) -``` - -**Memory Profile:** -- 1-2 allocations per message (vs 100+ for owned strings) -- Arena grows to largest message size, then stable -- Reset between messages = O(1) operation -- Per-connection memory = peak message size on that connection - ---- - -## Monoio + Monoio-http - -### Monoio Runtime - -**Thread-per-core execution:** -```rust -#[monoio::main(threads = 8)] // 8 worker threads -async fn main() { - let listener = TcpListener::bind("0.0.0.0:50051").unwrap(); - - loop { - let (stream, _) = listener.accept().await.unwrap(); - - // Spawn on same thread, never migrates - monoio::spawn(async move { - handle_connection(stream).await - }); - } -} -``` - -**Key properties:** -- Tasks spawn on specific thread, never move -- Can use `!Send` types (like arena-allocated messages) -- Work is distributed via accept() on multiple threads -- No work-stealing overhead - -### Monoio-http H2 Implementation - -**API:** -```rust -// Server-side HTTP/2 handling -let mut h2 = h2::server::handshake(stream).await?; - -while let Some(result) = h2.accept().await { - let (request, respond) = result?; - // request: http::Request - // respond: SendResponse -} -``` - -**Structure:** -- Complete HTTP/2 implementation in `monoio-http/src/h2/` -- Server and client support -- Stream multiplexing, flow control, settings frames -- Compatible with monoio's io_uring/legacy drivers - ---- - -## Arena Ownership Strategy - -### Per-Connection Arena - -Each connection handler owns its arena for the connection lifetime: - -```rust -async fn handle_connection(stream: TcpStream) -> Result<(), Error> { - // Connection owns arena (not pooled, not thread-local) - let mut arena = Arena::with_capacity(64 * 1024); - - let mut h2 = h2::server::handshake(stream).await?; - - while let Some(result) = h2.accept().await { - let (request, respond) = result?; - - // Decode request into arena - let message = codec.decode(request.body(), &arena)?; - - // Process (message borrows from arena) - let response = handler.handle(message, &arena).await?; - - // Encode response from same arena - let bytes = codec.encode(response, &arena)?; - respond.send_response(..., bytes)?; - - // Reset arena for next request - arena.reset(); - } - - // Arena dropped when connection closes - Ok(()) -} -``` - -**Why this works:** -1. Connection handler is `!Send` (contains arena) -2. Monoio tasks don't migrate, so `!Send` is fine -3. Arena scope = connection lifetime -4. Reset between requests = bounded memory -5. Each concurrent connection has its own arena - -**Memory profile:** -- 1000 concurrent connections = 1000 arenas -- Each arena sized to that connection's largest message -- 8 threads × 125 connections/thread × 4KB avg = ~4MB total - ---- - -## Service API Design - -### Service Trait Patterns - -**Unary RPC:** -```rust -#[async_trait(?Send)] -impl Greeter for MyGreeter { - async fn say_hello<'a>( - &self, - request: Request>, // View (borrowed) - arena: &'a Arena, // To build response - ) -> Result>, Status> { - let name = request.get_ref().name(); - - let mut reply = HelloReplyBuilder::new(arena); - reply.set_message(&format!("Hello {}", name)); - - Ok(Response::new(reply.build())) - } -} -``` - -**Server Streaming RPC:** -```rust -async fn stream_messages<'a>( - &self, - request: Request>, - mut send: impl FnMut(&'a Arena) -> Result, Status>, -) -> Result<(), Status> { - for i in 0..10 { - send(|arena| { - let mut resp = RespBuilder::new(arena); - resp.set_data(&format!("Message {}", i)); - Ok(resp.build()) - })?; - } - Ok(()) -} -``` - -**Client Streaming RPC:** -```rust -// Called once per incoming message -async fn on_message<'a>( - &mut self, - request: Req<'a>, -) -> Result<(), Status> { - self.accumulate(request); - Ok(()) -} - -// Called after stream ends -async fn finish<'a>( - &mut self, - arena: &'a Arena, -) -> Result>, Status> { - let mut resp = RespBuilder::new(arena); - resp.set_summary(&self.compute()); - Ok(Response::new(resp.build())) -} -``` - -**Bidirectional Streaming:** -```rust -// Called once per message, can return immediate response -async fn on_message<'a>( - &mut self, - request: Req<'a>, - arena: &'a Arena, -) -> Result>, Status> { - if should_respond(request) { - let mut resp = RespBuilder::new(arena); - resp.set_echo(request.text()); - Ok(Some(resp.build())) - } else { - Ok(None) // No response for this message - } -} -``` - -### Key API Differences from Tonic - -| Aspect | Tonic | Tocin | -|--------|-------|-------| -| Message ownership | Owned (`String`) | Borrowed (`&'a str`) | -| Arena parameter | None | Explicit `arena: &'a Arena` | -| Lifetime | No lifetimes | `<'a>` on messages | -| Async trait | `#[async_trait]` (Send) | `#[async_trait(?Send)]` | -| Client streaming | Single method | Split: `on_message` + `finish` | -| Server streaming | Returns Stream | Takes `send` callback | -| Runtime | tokio | monoio | - ---- - -## Implementation Tasks - -### 1. Update tocin-defiant Codec - -**Current Path:** `tocin-defiant/src/codec.rs:130-141` - -**Current decode path:** -``` -Streaming - └─> decode_chunk() [tocin/src/codec/decode.rs:375] - └─> decoder.decode(&mut decode_buf) - └─> ProstDecoder::decode() [tocin-defiant/src/codec.rs:130] - └─> Creates Arena::new() and immediately drops it -``` - -**Current:** -```rust -fn decode(&mut self, buf: &mut DecodeBuf<'_>) -> Result, Self::Error> { - let arena = Arena::new(); // Created per decode - let item = Decode::decode(buf, &arena)?; - Ok(item) // arena dropped, item uses owned String -} -``` - -**Target:** -```rust -fn decode<'a>( - &mut self, - buf: &mut DecodeBuf<'_>, - arena: &'a Arena, // Passed from connection handler -) -> Result>, Self::Error> { - let item = Decode::decode(buf, arena)?; - Ok(item) // item contains &'a str -} -``` - -**Decoder Trait Update:** -```rust -// Old -pub trait Decoder { - type Item; - fn decode(&mut self, buf: &mut DecodeBuf<'_>) -> Result, Self::Error>; -} - -// New (with arena and GAT) -pub trait Decoder { - type Item<'a>; // GAT for lifetime - fn decode<'a>( - &mut self, - buf: &mut DecodeBuf<'_>, - arena: &'a Arena, - ) -> Result>, Self::Error>; -} -``` - -### 2. Replace Transport Dependencies - -**File:** `tocin/Cargo.toml` - -**Remove:** -```toml -# tokio = "1.0" -# hyper = "1" -# hyper-util = "0.1.4" -# tower = "0.5" -# h2 = "0.4" -# axum = "0.8" -# tokio-stream = "0.1" -# tower-service = "0.3" -# tower-layer = "0.3" -# sync_wrapper = "1.0.2" -``` - -**Add:** -```toml -monoio = { version = "0.2", features = ["sync"] } -monoio-http = { version = "0.3" } -defiant = { path = "../defiant/defiant" } -monoio-rustls = { version = "0.1", optional = true } # For TLS -futures-core = "0.3" -futures-util = "0.3" -``` - -**Workspace Dependencies (root Cargo.toml):** -```toml -[workspace.dependencies] -monoio = { version = "0.2", features = ["sync"] } -monoio-http = { version = "0.3" } -monoio-rustls = { version = "0.1" } -defiant = { path = "../defiant/defiant" } -bytes = "1.0" -http = "1.1.0" -tracing = "0.1" -async-trait = "0.1.13" -``` - -### 3. Implement Monoio Transport Layer - -**Files to update:** -- `tocin/src/transport/server/mod.rs` - Replace hyper-based transport -- `tocin/src/server/grpc.rs` - Update Grpc handlers - -**monoio-http H2 API:** -```rust -// From monoio-http -pub fn handshake(io: T) -> Handshake -pub struct Connection { ... } - -impl Connection { - pub async fn accept(&mut self) - -> Option, SendResponse), Error>> -} - -pub struct SendResponse { - pub fn send_response(&mut self, response: Response<()>, end_of_stream: bool) - -> Result, Error> -} - -pub struct SendStream { - pub fn send_data(&mut self, data: Bytes, end_of_stream: bool) -> Result<(), Error> -} -``` - -**New implementation pattern:** -```rust -async fn handle_connection( - io: TcpStream, - mut service: S, -) -> Result<(), Error> { - let mut arena = Arena::new(); - let mut h2 = h2::server::handshake(io).await?; - - while let Some(result) = h2.accept().await { - let (request, mut respond) = result?; - - // Decode with arena - let message = decode(request.body(), &arena)?; - - // Call service - let response = service.call(message, &arena).await?; - - // Encode and send - let bytes = encode(response, &arena)?; - let mut send = respond.send_response( - http::Response::new(()), - false - )?; - send.send_data(bytes, true)?; - - arena.reset(); // Reuse for next request - } - - Ok(()) -} -``` - -**Memory Lifecycle per Streaming Type:** - -| Pattern | Arena Lifecycle | Peak Memory | -|---------|----------------|-------------| -| Unary | Request + Response in one arena, then reset | max(req_size, res_size) | -| Server Streaming | Request + each response | max(req_size, any_res_size) | -| Client Streaming | Each request individually | max(any_req_size, final_res_size) | -| Bidirectional | Each req/res pair | max(req_size + res_size) for any pair | - -### 4. Update Service Traits - -**Files:** -- `tocin/src/server/service.rs` - Server service traits -- `tocin/src/client/service.rs` - Client service traits - -**Remove:** -- `tower_service::Service` - replace with direct traits -- `tower_layer::Layer` - no middleware initially -- All tower compatibility blanket impls - -**Service Trait Bounds:** -```rust -#[async_trait(?Send)] // Critical: ?Send allows !Send futures -pub trait Greeter: Send + Sync + 'static { - async fn method<'a>(...) -> Result<...> -} -``` - -**Streaming Challenge Solution:** - -Problem: Standard `Stream` trait yields owned values, but arena items are borrowed. - -Solution: Per-message callback pattern instead of Stream - -```rust -// Framework manages stream loop -while let Some(data) = body.data().await { - arena.reset(); // Fresh arena for this message - let request = decode(&data, &arena)?; - service.on_message(request, &arena).await?; - // Both request and response dropped before arena.reset() -} -``` - -### 5. Update tocin-build Codegen - -**Files:** -- `tocin-build/src/` - Code generation logic -- `tocin/src/codegen.rs` - Codegen exports - -**Generated Trait Structure:** -```rust -// Generated by tocin-build -pub mod greeter_server { - use tocin::{async_trait, Request, Response, Status, Arena}; - - #[async_trait(?Send)] // Changed: added ?Send - pub trait Greeter: Send + Sync + 'static { - // Unary: added 'a lifetime and arena parameter - async fn say_hello<'a>( - &self, - request: Request>, // Changed: added 'a - arena: &'a Arena, // New parameter - ) -> Result>, Status>; // Changed: added 'a - - // Server streaming: added 'a and callback - async fn say_hello_stream<'a>( - &self, - request: Request>, - send: impl FnMut(&'a Arena) -> Result, Status>, - ) -> Result<(), Status>; - - // Client streaming: split into two methods - async fn say_hello_client_stream<'a>( - &mut self, - request: HelloRequest<'a>, - ) -> Result<(), Status>; - - async fn say_hello_client_stream_finish<'a>( - &mut self, - arena: &'a Arena, - ) -> Result>, Status>; - - // Bidirectional: added arena parameter - async fn say_hello_chat<'a>( - &mut self, - request: HelloRequest<'a>, - arena: &'a Arena, - ) -> Result>, Status>; - } -} -``` - -**Key Changes:** -- Add `<'a>` lifetime parameter to all methods -- Add `arena: &'a Arena` parameter -- Messages use `HelloRequest<'a>` instead of `HelloRequest` -- Client streaming split into `on_message` + `finish` methods -- Server streaming uses callback instead of returning Stream -- `#[async_trait(?Send)]` instead of `#[async_trait]` - -**View/Builder Lifetimes:** -```rust -// Views borrow from arena -pub struct HelloRequest<'a> { - name: &'a str, // Borrowed - age: i32, // Copied -} - -// Builders write to arena -pub struct HelloRequestBuilder<'a> { - arena: &'a Arena, - // fields... -} - -impl<'a> HelloRequestBuilder<'a> { - pub fn new(arena: &'a Arena) -> Self { ... } - pub fn set_name(&mut self, name: &str) { /* copies into arena */ } - pub fn build(self) -> HelloRequest<'a> { /* freezes to view */ } -} -``` - -### 6. Update Examples - -Convert examples like `helloworld/server.rs`: -```rust -#[monoio::main(threads = 4)] -async fn main() -> Result<(), Box> { - let greeter = MyGreeter::default(); - - tocin::transport::Server::builder() - .add_service(GreeterServer::new(greeter)) - .serve("0.0.0.0:50051") - .await?; - - Ok(()) -} -``` - ---- - -## Technical Challenges and Solutions - -### Challenge 1: Arena Lifetime Threading - -**Problem:** Current decode creates and drops arena per message, defeating the purpose. - -**Current decode path:** -``` -Streaming - └─> decode_chunk() [tocin/src/codec/decode.rs:375] - └─> decoder.decode(&mut decode_buf) - └─> ProstDecoder::decode() [tocin-defiant/src/codec.rs:130] - └─> Creates Arena::new() and immediately drops it -``` - -**Solution:** Pass arena from connection handler through entire decode path. Requires: -- Update `Decoder` trait with GAT: `type Item<'a>` -- Add `arena: &'a Arena` parameter to `decode()` -- Thread arena through all codec layers - -### Challenge 2: Streaming with Arena-Borrowed Data - -**Problem:** Standard `Stream` trait yields owned values, arena items are borrowed. - -**Solution:** Per-message callback pattern. Framework manages stream loop: -```rust -while let Some(data) = body.data().await { - arena.reset(); // Fresh arena for this message - let request = decode(&data, &arena)?; - service.on_message(request, &arena).await?; - // Both request and response dropped before arena.reset() -} -``` - -### Challenge 3: Borrow Checker Constraints - -**Problem:** Messages must not outlive arena. - -**Solution:** Enforced by lifetimes: -```rust -async fn handle_request<'a>( - request: Request<'a>, // Borrows from arena - arena: &'a Arena, -) -> Result, Status> { - // Can .await here - borrow is held across await points - // Other tasks on same thread have their own arenas - // Task is !Send so can't migrate threads - let response = process(request).await?; - Ok(response) // Response also borrows from arena -} -// Both request and response must be dropped before arena.reset() -``` - -**Connection Handler Constraints:** -```rust -// Handler must be !Send but that's OK with monoio -async fn handle_connection(stream: TcpStream) -> Result<(), Error> { - let mut arena = Arena::new(); // !Send - // ... entire handler stays on one thread - // Borrow checker ensures messages don't outlive arena -} -``` - ---- - -## Performance Targets - -### Current Tonic Benchmarks -``` -Small messages (12 bytes): 237-253ns -Medium messages (1KB): ~400ns -Large messages (84KB): 105µs -Many small allocations: 100+ per decode -``` - -### Target with Defiant + Arena -``` -Goal: <150ns for 10KB messages (~50% improvement) -``` - -**Expected improvements:** -- 1-2 allocations per message (vs 100+) -- Single-copy decode (vs zero-fill + copy) -- Zero allocations after arena warmup -- Direct memory reuse via arena.reset() - ---- - -## Migration Path - -### Phase 1: Foundation (Week 1) -- [ ] Update tocin-defiant codec to accept arena parameter -- [ ] Add monoio/monoio-http dependencies -- [ ] Remove tokio/hyper/tower dependencies - -### Phase 2: Core Transport (Week 2) -- [ ] Implement monoio-based server transport -- [ ] Update service traits with arena lifetimes -- [ ] Implement connection-scoped arena management - -### Phase 3: Codegen (Week 3) -- [ ] Update tocin-build to generate arena-aware traits -- [ ] Update generated code to use `?Send` -- [ ] Handle streaming patterns (split client streaming, etc.) - -### Phase 4: Polish (Week 4) -- [ ] Update all examples -- [ ] Run benchmarks and verify performance -- [ ] Documentation updates -- [ ] Test with real workloads - ---- - -## Open Questions - -1. **Client support**: Focus on server first, or implement client in parallel? -2. **Middleware**: How to replace tower layers with simple composition? -3. **Compression**: Keep gzip/deflate/zstd support? -4. **TLS**: monoio-rustls integration needed? -5. **Metrics**: How to expose connection/arena stats? - ---- - -## Key File Paths - -### Tocin Files to Modify - -**Core Transport & Runtime:** -- `tocin/Cargo.toml` - Dependency changes (tokio → monoio) -- `tocin/src/transport/server/mod.rs` - Server transport (hyper → monoio-http) -- `tocin/src/server/grpc.rs` - Core Grpc handlers with arena support -- `tocin/src/codec/mod.rs` - Codec traits (add arena parameter) -- `tocin/src/codec/decode.rs:375` - Streaming decode path - -**Service Layer:** -- `tocin/src/server/service.rs` - Service traits (remove tower, add arena) -- `tocin/src/client/service.rs` - Client service trait - -**Code Generation:** -- `tocin-build/src/` - Code generation logic -- `tocin/src/codegen.rs` - Codegen exports - -**Codec Implementation:** -- `tocin-defiant/src/codec.rs:130-141` - ProstDecoder (critical: arena lifetime) - -### External Dependencies - -**Local Repositories:** -- `/home/dan/Development/en/memory/tocin/` - Main tocin framework -- `/home/dan/Development/en/memory/defiant/` - Arena-based protobuf -- `/home/dan/Development/en/memory/monoio/` - Thread-per-core runtime (cached) -- `/home/dan/Development/en/memory/monoio-http/` - HTTP/2 implementation (cached) - -**Remote Repositories:** -- Defiant: `github.com/dwerner/defiant` -- Monoio: `github.com/bytedance/monoio` -- Monoio-http: `github.com/monoio-rs/monoio-http` -- Original tonic: `github.com/hyperium/tonic` - ---- - -## Summary of Key Decisions - -1. **Runtime:** Monoio (thread-per-core) instead of Tokio (work-stealing) - - Rationale: Arena references are `!Send`, require thread pinning - -2. **Arena Ownership:** Per-connection, not pooled or thread-local - - Each connection handler owns arena for its lifetime - - Reset between requests for bounded memory - -3. **Service API:** Explicit arena parameters, no `Stream` trait - - Methods take `arena: &'a Arena` parameter - - Server streaming uses callback pattern - - Client streaming split into `on_message` + `finish` - -4. **Trait Bounds:** `#[async_trait(?Send)]` everywhere - - Allows `!Send` futures containing arena references - - Safe because monoio tasks never migrate threads - -5. **Decoder Trait:** Generic Associated Type (GAT) for lifetimes - - `type Item<'a>` instead of `type Item` - - Enables arena-borrowed messages - -6. **No Tower/Hyper:** Direct integration with monoio-http - - Simpler stack, fewer abstractions - - Direct H2 connection handling - ---- - -## References - -- Defiant repo: `github.com/dwerner/defiant` -- Monoio: `https://github.com/bytedance/monoio` -- Monoio-http: `https://github.com/monoio-rs/monoio-http` -- Original tonic: `https://github.com/hyperium/tonic` diff --git a/PROST_ARENA_INTEGRATION.md b/PROST_ARENA_INTEGRATION.md deleted file mode 100644 index 19a26af..0000000 --- a/PROST_ARENA_INTEGRATION.md +++ /dev/null @@ -1,142 +0,0 @@ -# Prost Arena Integration into Tonic - -## Summary - -Successfully integrated the arena-based prost fork into tonic-prost, enabling tonic to use the new memory-efficient protobuf implementation. - -## Changes Made - -### 1. tonic-prost/Cargo.toml -- Updated prost dependency to use local path: `../../prost/prost` -- This pulls in the arena-based prost implementation - -### 2. tonic-prost/src/codec.rs - -#### Key Changes: -```rust -// Added Arena import -use prost::{Arena, Message}; - -// Updated trait bounds to support higher-ranked trait bounds (HRTB) -// This allows Message to work with any arena lifetime -T: for<'a> Message<'a> + Send + 'static, -U: for<'a> Message<'a> + Default + Send + 'static, - -// Updated ProstDecoder::decode to create arena per message -fn decode(&mut self, buf: &mut DecodeBuf<'_>) -> Result, Self::Error> { - // Create an arena for this decode operation - let arena = Arena::new(); - - // Decode using the arena - let item = Message::decode(buf, &arena) - .map(Option::Some) - .map_err(from_decode_error)?; - - // Return the decoded item (arena is dropped here, but owned data persists) - Ok(item) -} -``` - -## How It Works - -### Arena Lifetime Management - -The integration works because: - -1. **Per-Message Arena**: Each `decode()` call creates a fresh arena -2. **Owned Message Types**: Generated protobuf messages use `String` fields (owned data) -3. **Temporary Arena Use**: The arena is only used during the decode process -4. **Message Outlives Arena**: Owned `String` data is heap-allocated and outlives the arena - -### Example Flow: -``` -1. Client receives protobuf bytes from network -2. tonic calls ProstDecoder::decode(buf) -3. Arena created on stack -4. Message::decode(buf, &arena) called -5. Strings allocated on heap (not in arena) -6. Message returned -7. Arena dropped (no-op, strings still valid) -8. Message processed by application code -``` - -### Why This Works - -The key insight is that `Message<'arena>` is a **trait bound**, not a concrete type requirement: - -- Messages with `String` fields implement `Message<'arena>` **for any `'arena`** -- During decode, they allocate strings from the heap (via `String::from_utf8`) -- The arena is only used for temporary decode buffers -- The final message data is heap-allocated and independent of the arena - -## Performance Characteristics - -### Current Implementation (Owned Strings) -- **Decode**: ~176 ns per message (from benchmarks) -- **Allocations**: 8 per message (4 Strings + 4 temp Vecs) -- **Memory**: Fragmented across heap - -### Benefits from Arena API Integration -- Enables future migration to fully arena-allocated messages -- Provides infrastructure for zero-copy protobuf handling -- Maintains API compatibility with existing tonic code - -### Future Optimization Path -To get full arena benefits, we could: -1. Modify message lifetime to tie to request scope -2. Use arena-allocated string slices (`&'arena str`) -3. Box arena with message for longer-lived requests -4. Achieve ~139 ns decode time (20% faster from benchmarks) - -## Test Results - -All tonic-prost tests pass: -``` -running 5 tests -test codec::tests::decode ... ok -test codec::tests::encode_max_message_size_exceeded ... ok -test codec::tests::decode_max_message_size_exceeded ... ok -test codec::tests::encode ... ok -test codec::tests::encode_too_big ... ok - -test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out -``` - -## Remaining Work - -### High Priority -- [ ] Update prost-types to work with arena API (734 compile errors) -- [ ] Update prost-build code generation -- [ ] Run full tonic integration test suite - -### Future Enhancements -- [ ] Explore request-scoped arenas for full zero-copy benefits -- [ ] Benchmark tonic with arena-based prost -- [ ] Consider arena pooling for high-throughput scenarios - -## Technical Details - -### Higher-Ranked Trait Bounds (HRTB) - -The key to making this work was using HRTB: -```rust -T: for<'a> Message<'a> -``` - -This means: "T implements Message for **any** lifetime 'a" - -This allows the same type to be used with different arena lifetimes, making it compatible with tonic's ownership model where decoded messages must be `'static`. - -### Why Owned Messages Work - -Even though we pass an `&'arena Arena`, messages with owned `String` fields: -1. Ignore the arena for string storage -2. Allocate directly on the heap -3. Return a message with no lifetime ties to the arena -4. Work with tonic's `'static` requirement - -## Conclusion - -The integration successfully brings arena-based prost into tonic while maintaining backward compatibility. The current implementation uses owned message types, but the infrastructure is in place for future optimizations with zero-copy arena-allocated messages. - -The ~20% performance improvement seen in prost benchmarks can potentially be achieved in tonic by moving to request-scoped arenas in the future. diff --git a/REFACTOR_PROGRESS.md b/REFACTOR_PROGRESS.md deleted file mode 100644 index 3c7ec44..0000000 --- a/REFACTOR_PROGRESS.md +++ /dev/null @@ -1,223 +0,0 @@ -# Tocin Monoio Refactor Progress - -## Completed Tasks ✅ - -### 1. Updated Decoder Trait with GAT (Generic Associated Types) -- **File:** `tocin/src/codec/mod.rs` -- **Changes:** - - Changed `Decoder::Item` to `Decoder::Item<'a>` (GAT) - - Added `arena: &'a defiant::Arena` parameter to `decode()` method - - Updated `Codec` trait to use `type Decode<'a>` instead of `type Decode` - - Updated trait bounds to use `for<'a> Decoder = ...>` - -### 2. Updated tocin-defiant Codec Implementation -- **File:** `tocin-defiant/src/codec.rs` -- **Changes:** - - Implemented GAT in `ProstDecoder` - - Updated `decode()` to accept arena parameter - - Changed from creating temporary arena to using passed arena - - **Note:** Currently creates temp arena in `decode_chunk` (line 381 of decode.rs) - needs future refactor - -### 3. Removed tokio-stream Dependencies -- **Files:** - - `tocin/src/codec/encode.rs` - - `tocin/src/client/grpc.rs` - - `tocin/src/server/grpc.rs` - - `tocin/src/request.rs` - - `tocin/src/codec/decode.rs` -- **Changes:** - - Replaced `use tokio_stream::Stream` with `use futures_core::Stream` - - Replaced `use tokio_stream::StreamExt` with `use futures_util::StreamExt` - - Replaced `tokio_stream::adapters::Fuse` with `futures_util::stream::Fuse` - -### 4. Removed SyncWrapper -- **File:** `tocin/src/codec/decode.rs` -- **Changes:** - - Removed `SyncWrapper` from `Streaming` struct - - Changed `decoder: SyncWrapper>` to `decoder: Box<...>` - - Changed `body: SyncWrapper` to `body: Body` - - Updated all `SyncWrapper::new()` calls - - **Rationale:** Monoio is thread-per-core, so `!Send` types are acceptable - -### 5. Disabled Tower Dependencies -- **Files:** - - `tocin/src/service/interceptor.rs` - - `tocin/src/service/layered.rs` - - `tocin/src/service/recover_error.rs` - - `tocin/src/service/mod.rs` -- **Changes:** - - Commented out all tower-dependent code with `/* ... */` - - Kept `Interceptor` trait (core functionality) - - Added TODO comments for re-implementation - - Disabled exports in `service/mod.rs` - -## Current Status 🚧 - -### tocin Package Status -- ✅ Compiles with warnings only (no errors) -- ⚠️ Has unused import warnings -- ⚠️ Has feature flag warnings (expected - features removed from Cargo.toml) - -### Remaining Compilation Errors (Workspace Level) - -The main blocking issues are: - -1. **GAT Lifetime Parameters Missing** - `error[E0107]` - - `Streaming` needs to be `Streaming<...>` with proper lifetime handling - - `Codec` trait bounds need updating to `Decode<'a> = M2` - - Locations: - - `tocin/src/server/grpc.rs:401` - - `tocin/src/client/grpc.rs:218, 238, 274` - - **Root Cause:** `Streaming` type assumes `T` is a concrete type, but with GATs, decoded items have lifetime parameters - -2. **Streaming Type Refactor Needed** - - Current: `Streaming` where `T: 'static` - - Target: Need to handle `Streaming` where `T` has lifetime from arena - - This is a fundamental design change mentioned in INTEGRATION_PLAN.md - -3. **Missing tokio-stream in Other Packages** - - Several other workspace packages still use `tokio_stream` - - These will need similar refactoring - -## Next Steps 📋 - -### Immediate (Blocking Compilation) - -1. **Refactor Streaming Type** - - Option A: Make `Streaming` work with GAT types - - Option B: Use callback pattern instead of `Stream` trait (per INTEGRATION_PLAN.md) - - Option C: Temporary: Use owned types for now, optimize with arena later - -2. **Fix GAT Trait Bounds** - - Update all `Codec` to handle `Decode<'a>` - - May need higher-ranked trait bounds `for<'a> Codec = M>` - -### Medium Term - -3. **Remove Tokio-Based Transport Layer** - - Remove `tocin/src/transport/` hyper/tokio code - - Files to update: - - `tocin/src/transport/server/mod.rs` - - `tocin/src/transport/server/conn.rs` - - `tocin/src/transport/server/incoming.rs` - -4. **Create Monoio-Based Transport Layer** - - Implement using `monoio-http` - - Add arena support at connection level - - Per-connection arena ownership (not pooled) - -5. **Update Service Traits with Arena Lifetimes** - - Add `arena: &'a Arena` to service method signatures - - Change from `#[async_trait]` to `#[async_trait(?Send)]` - - Implement callback pattern for streaming - -6. **Update tocin-build Codegen** - - Generate traits with arena parameters - - Generate `#[async_trait(?Send)]` - - Split client streaming into `on_message` + `finish` - - Server streaming uses callback instead of Stream - -## Key Design Decisions - -### Arena Ownership Strategy -- **Per-connection arena** (not pooled, not thread-local) -- Connection handler owns arena for its lifetime -- Reset between requests for bounded memory -- Memory profile: `num_connections × peak_message_size` - -### Runtime -- **Monoio** (thread-per-core) instead of Tokio (work-stealing) -- Tasks never migrate threads (`!Send` is acceptable) -- Rationale: Arena references are `!Send`, require thread pinning - -### Stream Handling -- **Callback pattern** instead of `Stream` trait -- Framework manages stream loop -- User implements per-message handlers -- Solves lifetime issues with arena-borrowed data - -## Files Modified - -### Core Codec Layer -- ✅ `tocin/src/codec/mod.rs` - Decoder trait with GAT -- ✅ `tocin/src/codec/decode.rs` - Updated Streaming (partial) -- ✅ `tocin/src/codec/encode.rs` - Removed tokio-stream -- ✅ `tocin-defiant/src/codec.rs` - Arena-aware decoder - -### Service Layer -- ✅ `tocin/src/service/interceptor.rs` - Tower code disabled -- ✅ `tocin/src/service/layered.rs` - Tower code disabled -- ✅ `tocin/src/service/recover_error.rs` - Tower code disabled -- ✅ `tocin/src/service/mod.rs` - Updated exports - -### gRPC Handlers -- ✅ `tocin/src/server/grpc.rs` - Removed tokio-stream -- ✅ `tocin/src/client/grpc.rs` - Removed tokio-stream -- ⚠️ Both need GAT updates - -### Other -- ✅ `tocin/src/request.rs` - Removed tokio-stream - -## Dependencies Status - -### Removed (from tocin/Cargo.toml) -- ✅ tokio (except features still used by other deps) -- ✅ hyper -- ✅ hyper-util -- ✅ tower -- ✅ tower-service -- ✅ tower-layer -- ✅ h2 -- ✅ axum -- ✅ tokio-stream -- ✅ sync_wrapper - -### Added (to workspace Cargo.toml) -- ✅ monoio = { version = "0.2", features = ["sync"] } -- ✅ monoio-http = "0.3" -- ✅ defiant = { path = "../defiant/defiant" } -- ✅ futures-core = "0.3" -- ✅ futures-util = "0.3" - -## Technical Debt / TODOs - -1. **Arena in decode_chunk** (`tocin/src/codec/decode.rs:381`) - - Currently creates temp arena per decode - - Need to thread arena from connection handler - -2. **Tower Integration** - - All tower-dependent middleware is disabled - - Need to decide: re-implement without tower, or re-add tower conditionally - -3. **Feature Flags** - - Removed `server`, `channel`, `router` features from Cargo.toml - - Getting warnings about these missing features - - Need to restore or update feature flag usage - -4. **Streaming Refactor** - - Major: Replace `Stream` trait with callback pattern - - See INTEGRATION_PLAN.md sections on "Streaming Challenge" - -## Performance Targets - -From INTEGRATION_PLAN.md: - -### Current Tonic -- Small messages (12 bytes): 237-253ns -- Medium messages (1KB): ~400ns -- Large messages (84KB): 105µs -- Many allocations: 100+ per decode - -### Target with Defiant + Arena -- Goal: <150ns for 10KB messages (~50% improvement) -- 1-2 allocations per message (vs 100+) -- Single-copy decode -- Zero allocations after arena warmup - -## References - -- INTEGRATION_PLAN.md - Complete integration strategy -- /home/dan/Development/en/memory/conversation_including_api.txt - Design discussions -- Defiant: github.com/dwerner/defiant -- Monoio: github.com/bytedance/monoio -- Monoio-http: github.com/monoio-rs/monoio-http diff --git a/examples/Cargo.toml b/examples/Cargo.toml index 94092b0..7625c24 100644 --- a/examples/Cargo.toml +++ b/examples/Cargo.toml @@ -170,16 +170,6 @@ name = "mock" path = "src/mock/mock.rs" required-features = ["mock"] -[[bin]] -name = "grpc-web-server" -path = "src/grpc-web/server.rs" -required-features = ["grpc-web"] - -[[bin]] -name = "grpc-web-client" -path = "src/grpc-web/client.rs" -required-features = ["grpc-web"] - [[bin]] name = "streaming-client" path = "src/streaming/client.rs" @@ -253,57 +243,51 @@ path = "src/codec_buffers/client.rs" [features] -gcp = ["dep:prost-types", "tocin/tls-ring"] -routeguide = ["dep:async-stream", "dep:tokio-stream", "dep:rand", "dep:serde", "dep:serde_json"] +gcp = ["dep:defiant-types", "tocin/tls"] +routeguide = ["dep:async-stream", "dep:rand", "dep:serde", "dep:serde_json"] reflection = ["dep:tocin-reflection"] -autoreload = ["dep:tokio-stream", "tokio-stream?/net", "dep:listenfd"] +autoreload = ["dep:listenfd"] health = ["dep:tocin-health"] -grpc-web = ["dep:tocin-web", "dep:bytes", "dep:http", "dep:hyper", "dep:hyper-util", "dep:tracing-subscriber", "dep:tower", "dep:tower-http", "tower-http?/cors"] +# grpc-web removed: not appropriate for perf-oriented thread-per-core servers tracing = ["dep:tracing", "dep:tracing-subscriber"] -uds = ["dep:tokio-stream", "tokio-stream?/net", "dep:tower", "dep:hyper", "dep:hyper-util"] -streaming = ["dep:tokio-stream", "dep:h2"] -mock = ["dep:tokio-stream", "dep:tower", "dep:hyper-util"] +uds = ["dep:tower", "dep:monoio-http"] +streaming = ["dep:monoio-http"] +mock = ["dep:tower", "dep:monoio-http"] json-codec = ["dep:serde", "dep:serde_json", "dep:bytes"] compression = ["tocin/gzip"] -tls = ["tocin/tls-ring"] -tls-rustls = ["dep:http", "dep:hyper", "dep:hyper-util", "dep:hyper-rustls", "dep:tower", "tower-http/util", "tower-http/add-extension", "dep:tokio-rustls"] -tls-client-auth = ["tocin/tls-ring"] +tls = ["tocin/tls"] +tls-rustls = ["dep:http", "dep:monoio-http", "dep:tower", "tower-http/util", "tower-http/add-extension"] +tls-client-auth = ["tocin/tls"] types = ["dep:tocin-types"] -h2c = ["dep:hyper", "dep:tower", "dep:http", "dep:hyper-util"] -cancellation = ["dep:tokio-util"] +h2c = ["dep:monoio-http", "dep:tower", "dep:http"] +cancellation = [] -full = ["gcp", "routeguide", "reflection", "autoreload", "health", "grpc-web", "tracing", "uds", "streaming", "mock", "json-codec", "compression", "tls", "tls-rustls", "tls-client-auth", "types", "cancellation", "h2c"] -default = ["full"] +full = [] +default = [] [dependencies] # Common dependencies -tokio = { version = "1.0", features = ["rt-multi-thread", "macros"] } -prost = "0.14" +monoio = { workspace = true, features = ["macros"] } +defiant = { workspace = true } tocin = { path = "../tocin" } tocin-defiant = { path = "../tocin-defiant" } # Optional dependencies -tocin-web = { path = "../tocin-web", optional = true } +# tocin-web removed tocin-health = { path = "../tocin-health", optional = true } tocin-reflection = { path = "../tocin-reflection", optional = true } tocin-types = { path = "../tocin-types", optional = true } async-stream = { version = "0.3", optional = true } -tokio-stream = { version = "0.1", optional = true } -tokio-util = { version = "0.7.8", optional = true } tower = { version = "0.5", optional = true } rand = { version = "0.9", optional = true } serde = { version = "1.0", features = ["derive"], optional = true } serde_json = { version = "1.0", optional = true } tracing = { version = "0.1.16", optional = true } tracing-subscriber = { version = "0.3", features = ["tracing-log", "fmt"], optional = true } -prost-types = { version = "0.14", optional = true } +defiant-types = { workspace = true, optional = true } http = { version = "1", optional = true } -hyper = { version = "1", optional = true } -hyper-util = { version = "0.1.4", optional = true } +monoio-http = { version = "0.3", optional = true } listenfd = { version = "1.0", optional = true } bytes = { version = "1", optional = true } -h2 = { version = "0.4", optional = true } -tokio-rustls = { version = "0.26.1", optional = true, features = ["ring", "tls12"], default-features = false } -hyper-rustls = { version = "0.27.0", features = ["http2", "ring", "tls12"], optional = true, default-features = false } tower-http = { version = "0.6", optional = true } [build-dependencies] diff --git a/examples/src/grpc-web/client.rs b/examples/src/grpc-web/client.rs deleted file mode 100644 index 4ded3df..0000000 --- a/examples/src/grpc-web/client.rs +++ /dev/null @@ -1,29 +0,0 @@ -use hello_world::{greeter_client::GreeterClient, HelloRequest}; -use hyper_util::rt::TokioExecutor; -use tocin_web::GrpcWebClientLayer; - -pub mod hello_world { - tocin::include_proto!("helloworld"); -} - -#[tokio::main] -async fn main() -> Result<(), Box> { - // Must use hyper directly... - let client = hyper_util::client::legacy::Client::builder(TokioExecutor::new()).build_http(); - - let svc = tower::ServiceBuilder::new() - .layer(GrpcWebClientLayer::new()) - .service(client); - - let mut client = GreeterClient::with_origin(svc, "http://127.0.0.1:3000".try_into()?); - - let request = tocin::Request::new(HelloRequest { - name: "Tonic".into(), - }); - - let response = client.say_hello(request).await?; - - println!("RESPONSE={response:?}"); - - Ok(()) -} diff --git a/examples/src/grpc-web/server.rs b/examples/src/grpc-web/server.rs deleted file mode 100644 index 4daf1f7..0000000 --- a/examples/src/grpc-web/server.rs +++ /dev/null @@ -1,51 +0,0 @@ -use tocin::{service::LayerExt as _, transport::Server, Request, Response, Status}; - -use hello_world::greeter_server::{Greeter, GreeterServer}; -use hello_world::{HelloReply, HelloRequest}; - -pub mod hello_world { - tocin::include_proto!("helloworld"); -} - -#[derive(Default)] -pub struct MyGreeter {} - -#[tocin::async_trait] -impl Greeter for MyGreeter { - async fn say_hello( - &self, - request: Request, - ) -> Result, Status> { - println!("Got a request from {:?}", request.remote_addr()); - - let reply = hello_world::HelloReply { - message: format!("Hello {}!", request.into_inner().name), - }; - Ok(Response::new(reply)) - } -} - -#[tokio::main] -async fn main() -> Result<(), Box> { - tracing_subscriber::fmt::init(); - - let addr = "127.0.0.1:3000".parse().unwrap(); - - let greeter = MyGreeter::default(); - let greeter = tower::ServiceBuilder::new() - .layer(tower_http::cors::CorsLayer::new()) - .layer(tocin_web::GrpcWebLayer::new()) - .into_inner() - .named_layer(GreeterServer::new(greeter)); - - println!("GreeterServer listening on {addr}"); - - Server::builder() - // GrpcWeb is over http1 so we must enable it. - .accept_http1(true) - .add_service(greeter) - .serve(addr) - .await?; - - Ok(()) -} diff --git a/examples/src/helloworld/client.rs b/examples/src/helloworld/client.rs index a59f913..5e17087 100644 --- a/examples/src/helloworld/client.rs +++ b/examples/src/helloworld/client.rs @@ -5,12 +5,14 @@ pub mod hello_world { tocin::include_proto!("helloworld"); } -#[tokio::main] +#[monoio::main(timer_enabled = true)] async fn main() -> Result<(), Box> { - let mut client = GreeterClient::connect("http://[::1]:50051").await?; + let arena = defiant::Arena::new(); + + let mut client = GreeterClient::connect(&arena, "http://[::1]:50051").await?; let request = tocin::Request::new(HelloRequest { - name: "Tonic".into(), + name: "Tocin", }); let response = client.say_hello(request).await?; diff --git a/examples/src/helloworld/server.rs b/examples/src/helloworld/server.rs index 61ac63a..c09bb09 100644 --- a/examples/src/helloworld/server.rs +++ b/examples/src/helloworld/server.rs @@ -10,32 +10,39 @@ pub mod hello_world { #[derive(Default)] pub struct MyGreeter {} -#[tocin::async_trait] +#[tocin::async_trait(?Send)] impl Greeter for MyGreeter { - async fn say_hello( + async fn say_hello<'arena>( &self, - request: Request, - ) -> Result, Status> { + arena: &'arena defiant::Arena, + request: Request>, + ) -> Result>, Status> { println!("Got a request from {:?}", request.remote_addr()); + let name = request.into_inner().name; + let message = arena.alloc_str(&format!("Hello {}!", name)); + let reply = hello_world::HelloReply { - message: format!("Hello {}!", request.into_inner().name), + message, }; Ok(Response::new(reply)) } } -#[tokio::main] -async fn main() -> Result<(), Box> { - let addr = "[::1]:50051".parse().unwrap(); - let greeter = MyGreeter::default(); +fn main() -> Result<(), Box> { + let mut runtime = monoio::RuntimeBuilder::::new() + .build() + .expect("Failed to create runtime"); + + runtime.block_on(async { + let addr = "[::1]:50051".parse().unwrap(); + let greeter_service = GreeterServer::new(MyGreeter::default()); - println!("GreeterServer listening on {addr}"); + println!("GreeterServer listening on {addr}"); - Server::builder() - .add_service(GreeterServer::new(greeter)) - .serve(addr) - .await?; + let server = Server::bind(addr).await?; + server.serve(greeter_service).await?; - Ok(()) + Ok(()) + }) } diff --git a/tests/web/Cargo.toml b/tests/web/Cargo.toml deleted file mode 100644 index af6d4d4..0000000 --- a/tests/web/Cargo.toml +++ /dev/null @@ -1,23 +0,0 @@ -[package] -authors = ["Juan Alvarez "] -edition = "2021" -name = "test_web" -license = "MIT" - -[dependencies] -base64 = "0.22" -bytes = "1.0" -http-body-util = "0.1" -hyper = "1" -hyper-util = "0.1" -prost = "0.14" -tokio = { version = "1", features = ["macros", "rt", "net"] } -tokio-stream = { version = "0.1", features = ["net"] } -tocin = { path = "../../tocin" } -tocin-defiant = { path = "../../tocin-defiant" } - -[dev-dependencies] -tocin-web = { path = "../../tocin-web" } - -[build-dependencies] -tocin-defiant-build = { path = "../../tocin-defiant-build" } diff --git a/tests/web/build.rs b/tests/web/build.rs deleted file mode 100644 index 1d9ce4b..0000000 --- a/tests/web/build.rs +++ /dev/null @@ -1,11 +0,0 @@ -fn main() { - let protos = &["proto/test.proto"]; - - tocin_defiant_build::configure() - .compile_protos(protos, &["proto"]) - .unwrap(); - - protos - .iter() - .for_each(|file| println!("cargo:rerun-if-changed={file}")); -} diff --git a/tests/web/proto/test.proto b/tests/web/proto/test.proto deleted file mode 100644 index 76e01fd..0000000 --- a/tests/web/proto/test.proto +++ /dev/null @@ -1,19 +0,0 @@ -syntax = "proto3"; - -package test; - -service Test { - rpc UnaryCall(Input) returns (Output); - rpc ServerStream(Input) returns (stream Output); - rpc ClientStream(stream Input) returns (Output); -} - -message Input { - int32 id = 1; - string desc = 2; -} - -message Output { - int32 id = 1; - string desc = 2; -} diff --git a/tests/web/src/lib.rs b/tests/web/src/lib.rs deleted file mode 100644 index f6ced3a..0000000 --- a/tests/web/src/lib.rs +++ /dev/null @@ -1,88 +0,0 @@ -use std::pin::Pin; - -use tokio_stream::{self as stream, Stream, StreamExt}; -use tocin::{Request, Response, Status, Streaming}; - -use pb::{test_server::Test, Input, Output}; - -pub mod pb { - tocin::include_proto!("test"); -} - -type BoxStream = Pin> + Send + 'static>>; - -pub struct Svc; - -#[tocin::async_trait] -impl Test for Svc { - async fn unary_call(&self, req: Request) -> Result, Status> { - let req = req.into_inner(); - - if &req.desc == "boom" { - Err(Status::invalid_argument("invalid boom")) - } else { - Ok(Response::new(Output { - id: req.id, - desc: req.desc, - })) - } - } - - type ServerStreamStream = BoxStream; - - async fn server_stream( - &self, - req: Request, - ) -> Result, Status> { - let req = req.into_inner(); - - Ok(Response::new(Box::pin(stream::iter(vec![1, 2]).map( - move |n| { - Ok(Output { - id: req.id, - desc: format!("{}-{}", n, req.desc), - }) - }, - )))) - } - - async fn client_stream( - &self, - req: Request>, - ) -> Result, Status> { - let out = Output { - id: 0, - desc: "".into(), - }; - - Ok(Response::new( - req.into_inner() - .fold(out, |mut acc, input| { - let input = input.unwrap(); - acc.id += input.id; - acc.desc += &input.desc; - acc - }) - .await, - )) - } -} - -pub mod util { - pub mod base64 { - use base64::{ - alphabet, - engine::{ - general_purpose::{GeneralPurpose, GeneralPurposeConfig}, - DecodePaddingMode, - }, - }; - - pub const STANDARD: GeneralPurpose = GeneralPurpose::new( - &alphabet::STANDARD, - GeneralPurposeConfig::new() - .with_encode_padding(true) - .with_decode_padding_mode(DecodePaddingMode::Indifferent), - ); - } -} diff --git a/tests/web/tests/grpc.rs b/tests/web/tests/grpc.rs deleted file mode 100644 index 1501095..0000000 --- a/tests/web/tests/grpc.rs +++ /dev/null @@ -1,165 +0,0 @@ -use std::future::Future; -use std::net::SocketAddr; - -use tokio::net::TcpListener; -use tokio::time::Duration; -use tokio::{join, try_join}; -use tokio_stream::wrappers::TcpListenerStream; -use tokio_stream::{self as stream, StreamExt}; -use tocin::transport::{Channel, Error, Server}; -use tocin::{Response, Streaming}; - -use test_web::pb::{test_client::TestClient, test_server::TestServer, Input}; -use test_web::Svc; -use tocin_web::GrpcWebLayer; - -#[tokio::test] -async fn smoke_unary() { - let (mut c1, mut c2, mut c3, mut c4) = spawn().await.expect("clients"); - - let (r1, r2, r3, r4) = try_join!( - c1.unary_call(input()), - c2.unary_call(input()), - c3.unary_call(input()), - c4.unary_call(input()), - ) - .expect("responses"); - - assert!(meta(&r1) == meta(&r2) && meta(&r2) == meta(&r3) && meta(&r3) == meta(&r4)); - assert!(data(&r1) == data(&r2) && data(&r2) == data(&r3) && data(&r3) == data(&r4)); -} - -#[tokio::test] -async fn smoke_client_stream() { - let (mut c1, mut c2, mut c3, mut c4) = spawn().await.expect("clients"); - - let input_stream = || stream::iter(vec![input(), input()]); - - let (r1, r2, r3, r4) = try_join!( - c1.client_stream(input_stream()), - c2.client_stream(input_stream()), - c3.client_stream(input_stream()), - c4.client_stream(input_stream()), - ) - .expect("responses"); - - assert!(meta(&r1) == meta(&r2) && meta(&r2) == meta(&r3) && meta(&r3) == meta(&r4)); - assert!(data(&r1) == data(&r2) && data(&r2) == data(&r3) && data(&r3) == data(&r4)); -} - -#[tokio::test] -async fn smoke_server_stream() { - let (mut c1, mut c2, mut c3, mut c4) = spawn().await.expect("clients"); - - let (r1, r2, r3, r4) = try_join!( - c1.server_stream(input()), - c2.server_stream(input()), - c3.server_stream(input()), - c4.server_stream(input()), - ) - .expect("responses"); - - assert!(meta(&r1) == meta(&r2) && meta(&r2) == meta(&r3) && meta(&r3) == meta(&r4)); - - let r1 = stream(r1).await; - let r2 = stream(r2).await; - let r3 = stream(r3).await; - let r4 = stream(r4).await; - - assert!(r1 == r2 && r2 == r3 && r3 == r4); -} -#[tokio::test] -async fn smoke_error() { - let (mut c1, mut c2, mut c3, mut c4) = spawn().await.expect("clients"); - - let boom = Input { - id: 1, - desc: "boom".to_owned(), - }; - - let (r1, r2, r3, r4) = join!( - c1.unary_call(boom.clone()), - c2.unary_call(boom.clone()), - c3.unary_call(boom.clone()), - c4.unary_call(boom.clone()), - ); - - let s1 = r1.unwrap_err(); - let s2 = r2.unwrap_err(); - let s3 = r3.unwrap_err(); - let s4 = r4.unwrap_err(); - - assert!(status(&s1) == status(&s2) && status(&s2) == status(&s3) && status(&s3) == status(&s4)) -} - -async fn bind() -> (TcpListener, String) { - let addr = SocketAddr::from(([127, 0, 0, 1], 0)); - let lis = TcpListener::bind(addr).await.expect("listener"); - let url = format!("http://{}", lis.local_addr().unwrap()); - - (lis, url) -} - -async fn grpc(accept_h1: bool) -> (impl Future>, String) { - let (listener, url) = bind().await; - - let fut = Server::builder() - .accept_http1(accept_h1) - .add_service(TestServer::new(Svc)) - .serve_with_incoming(TcpListenerStream::new(listener)); - - (fut, url) -} - -async fn grpc_web(accept_h1: bool) -> (impl Future>, String) { - let (listener, url) = bind().await; - - let fut = Server::builder() - .accept_http1(accept_h1) - .layer(GrpcWebLayer::new()) - .add_service(TestServer::new(Svc)) - .serve_with_incoming(TcpListenerStream::new(listener)); - - (fut, url) -} - -type Client = TestClient; - -async fn spawn() -> Result<(Client, Client, Client, Client), Error> { - let ((s1, u1), (s2, u2), (s3, u3), (s4, u4)) = - join!(grpc(true), grpc(false), grpc_web(true), grpc_web(false)); - - drop(tokio::spawn(async move { join!(s1, s2, s3, s4) })); - - tokio::time::sleep(Duration::from_millis(30)).await; - - try_join!( - TestClient::connect(u1), - TestClient::connect(u2), - TestClient::connect(u3), - TestClient::connect(u4) - ) -} - -fn input() -> Input { - Input { - id: 1, - desc: "one".to_owned(), - } -} - -fn meta(r: &Response) -> String { - format!("{:?}", r.metadata()) -} - -fn data(r: &Response) -> &T { - r.get_ref() -} - -async fn stream(r: Response>) -> Vec { - r.into_inner().collect::, _>>().await.unwrap() -} - -fn status(s: &tocin::Status) -> (String, tocin::Code) { - (format!("{:?}", s.metadata()), s.code()) -} diff --git a/tests/web/tests/grpc_web.rs b/tests/web/tests/grpc_web.rs deleted file mode 100644 index 82c0513..0000000 --- a/tests/web/tests/grpc_web.rs +++ /dev/null @@ -1,154 +0,0 @@ -use std::net::SocketAddr; - -use base64::Engine as _; -use bytes::{Buf, BufMut, Bytes, BytesMut}; -use http_body_util::{BodyExt as _, Full}; -use hyper::body::Incoming; -use hyper::http::{header, StatusCode}; -use hyper::{Method, Request, Uri}; -use hyper_util::client::legacy::Client; -use hyper_util::rt::TokioExecutor; -use prost::Message; -use tokio::net::TcpListener; -use tokio_stream::wrappers::TcpListenerStream; -use tocin::body::Body; -use tocin::transport::Server; - -use test_web::pb::{test_server::TestServer, Input, Output}; -use test_web::Svc; -use tocin::Status; -use tocin_web::GrpcWebLayer; - -#[tokio::test] -async fn binary_request() { - let server_url = spawn().await; - let client = Client::builder(TokioExecutor::new()).build_http(); - - let req = build_request(server_url, "grpc-web", "grpc-web"); - let res = client.request(req).await.unwrap(); - let content_type = res.headers().get(header::CONTENT_TYPE).unwrap().clone(); - let content_type = content_type.to_str().unwrap(); - - assert_eq!(res.status(), StatusCode::OK); - assert_eq!(content_type, "application/grpc-web+proto"); - - let (message, trailers) = decode_body(res.into_body(), content_type).await; - let expected = Output { - id: 1, - desc: "one".to_owned(), - }; - - assert_eq!(message, expected); - assert_eq!(&trailers[..], b"grpc-status:0\r\n"); -} - -#[tokio::test] -async fn text_request() { - let server_url = spawn().await; - let client = Client::builder(TokioExecutor::new()).build_http(); - - let req = build_request(server_url, "grpc-web-text", "grpc-web-text"); - let res = client.request(req).await.unwrap(); - let content_type = res.headers().get(header::CONTENT_TYPE).unwrap().clone(); - let content_type = content_type.to_str().unwrap(); - - assert_eq!(res.status(), StatusCode::OK); - assert_eq!(content_type, "application/grpc-web-text+proto"); - - let (message, trailers) = decode_body(res.into_body(), content_type).await; - let expected = Output { - id: 1, - desc: "one".to_owned(), - }; - - assert_eq!(message, expected); - assert_eq!(&trailers[..], b"grpc-status:0\r\n"); -} - -async fn spawn() -> String { - let addr = SocketAddr::from(([127, 0, 0, 1], 0)); - let listener = TcpListener::bind(addr).await.expect("listener"); - let url = format!("http://{}", listener.local_addr().unwrap()); - let listener_stream = TcpListenerStream::new(listener); - - drop(tokio::spawn(async move { - Server::builder() - .accept_http1(true) - .layer(GrpcWebLayer::new()) - .add_service(TestServer::new(Svc)) - .serve_with_incoming(listener_stream) - .await - .unwrap() - })); - - url -} - -fn encode_body() -> Bytes { - let input = Input { - id: 1, - desc: "one".to_owned(), - }; - - let mut buf = BytesMut::with_capacity(1024); - buf.reserve(5); - unsafe { - buf.advance_mut(5); - } - - input.encode(&mut buf).unwrap(); - - let len = buf.len() - 5; - { - let mut buf = &mut buf[..5]; - buf.put_u8(0); - buf.put_u32(len as u32); - } - - buf.split_to(len + 5).freeze() -} - -fn build_request(base_uri: String, content_type: &str, accept: &str) -> Request { - use header::{ACCEPT, CONTENT_TYPE, ORIGIN}; - - let request_uri = format!("{}/{}/{}", base_uri, "test.Test", "UnaryCall") - .parse::() - .unwrap(); - - let bytes = match content_type { - "grpc-web" => encode_body(), - "grpc-web-text" => test_web::util::base64::STANDARD - .encode(encode_body()) - .into(), - _ => panic!("invalid content type {content_type}"), - }; - - Request::builder() - .method(Method::POST) - .header(CONTENT_TYPE, format!("application/{content_type}")) - .header(ORIGIN, "http://example.com") - .header(ACCEPT, format!("application/{accept}")) - .uri(request_uri) - .body(Body::new( - Full::new(bytes).map_err(|err| Status::internal(err.to_string())), - )) - .unwrap() -} - -async fn decode_body(body: Incoming, content_type: &str) -> (Output, Bytes) { - let mut body = body.collect().await.unwrap().to_bytes(); - - if content_type == "application/grpc-web-text+proto" { - body = test_web::util::base64::STANDARD - .decode(body) - .unwrap() - .into() - } - - body.advance(1); - let len = body.get_u32(); - let msg = Output::decode(&mut body.split_to(len as usize)).expect("decode"); - body.advance(5); - - (msg, body) -} diff --git a/tocin-build/src/client.rs b/tocin-build/src/client.rs index 6b67282..385bb8d 100644 --- a/tocin-build/src/client.rs +++ b/tocin-build/src/client.rs @@ -59,41 +59,29 @@ pub(crate) fn generate_internal( #service_doc #(#struct_attributes)* #[derive(Debug, Clone)] - pub struct #service_ident { - inner: tocin::client::Grpc, + pub struct #service_ident<'arena, T> { + inner: tocin::client::Grpc<'arena, T>, } #connect - impl #service_ident + impl<'arena, T> #service_ident<'arena, T> where - T: tocin::client::GrpcService, + T: tocin::client::GrpcService, T::Error: Into, - T::ResponseBody: Body + std::marker::Send + 'static, - ::Error: Into + std::marker::Send, { - pub fn new(inner: T) -> Self { - let inner = tocin::client::Grpc::new(inner); + pub fn new(inner: T, arena: &'arena defiant::Arena) -> Self { + let inner = tocin::client::Grpc::new(inner, arena); Self { inner } } - pub fn with_origin(inner: T, origin: Uri) -> Self { - let inner = tocin::client::Grpc::with_origin(inner, origin); + pub fn with_origin(inner: T, arena: &'arena defiant::Arena, origin: Uri) -> Self { + let inner = tocin::client::Grpc::with_origin(inner, arena, origin); Self { inner } } - pub fn with_interceptor(inner: T, interceptor: F) -> #service_ident> - where - F: tocin::service::Interceptor, - T::ResponseBody: Default, - T: tocin::codegen::Service< - http::Request, - Response = http::Response<>::ResponseBody> - >, - >>::Error: Into + std::marker::Send + std::marker::Sync, - { - #service_ident::new(InterceptedService::new(inner, interceptor)) - } + // Note: with_interceptor is not supported in tocin due to monoio's !Send constraint + // Interceptors would require tower::Service which expects Send futures /// Compress requests with the given encoding. /// @@ -139,15 +127,16 @@ pub(crate) fn generate_internal( #[cfg(feature = "transport")] fn generate_connect(service_ident: &syn::Ident, enabled: bool) -> TokenStream { let connect_impl = quote! { - impl #service_ident { + impl<'arena> #service_ident<'arena, tocin::transport::Channel> { /// Attempt to create a new client by connecting to a given endpoint. - pub async fn connect(dst: D) -> Result + pub async fn connect(arena: &'arena defiant::Arena, dst: D) -> Result where D: TryInto, D::Error: Into, { let conn = tocin::transport::Endpoint::new(dst)?.connect().await?; - Ok(Self::new(conn)) + let origin = conn.uri().clone(); + Ok(Self::with_origin(conn, arena, origin)) } } }; @@ -237,9 +226,6 @@ fn generate_unary( &mut self, request: impl tocin::IntoRequest<#request>, ) -> std::result::Result, tocin::Status> { - self.inner.ready().await.map_err(|e| { - tocin::Status::unknown(format!("Service was not ready: {}", e.into())) - })?; let codec = #codec_name::default(); let path = http::uri::PathAndQuery::from_static(#path); let mut req = request.into_request(); @@ -267,10 +253,7 @@ fn generate_server_streaming( pub async fn #ident( &mut self, request: impl tocin::IntoRequest<#request>, - ) -> std::result::Result>, tocin::Status> { - self.inner.ready().await.map_err(|e| { - tocin::Status::unknown(format!("Service was not ready: {}", e.into())) - })?; + ) -> std::result::Result>, tocin::Status> { let codec = #codec_name::default(); let path = http::uri::PathAndQuery::from_static(#path); let mut req = request.into_request(); @@ -299,9 +282,6 @@ fn generate_client_streaming( &mut self, request: impl tocin::IntoStreamingRequest ) -> std::result::Result, tocin::Status> { - self.inner.ready().await.map_err(|e| { - tocin::Status::unknown(format!("Service was not ready: {}", e.into())) - })?; let codec = #codec_name::default(); let path = http::uri::PathAndQuery::from_static(#path); let mut req = request.into_streaming_request(); @@ -329,10 +309,7 @@ fn generate_streaming( pub async fn #ident( &mut self, request: impl tocin::IntoStreamingRequest - ) -> std::result::Result>, tocin::Status> { - self.inner.ready().await.map_err(|e| { - tocin::Status::unknown(format!("Service was not ready: {}", e.into())) - })?; + ) -> std::result::Result>, tocin::Status> { let codec = #codec_name::default(); let path = http::uri::PathAndQuery::from_static(#path); let mut req = request.into_streaming_request(); diff --git a/tocin-build/src/server.rs b/tocin-build/src/server.rs index 8cb1947..4169012 100644 --- a/tocin-build/src/server.rs +++ b/tocin-build/src/server.rs @@ -136,29 +136,29 @@ pub(crate) fn generate_internal( } } - pub fn with_interceptor(inner: T, interceptor: F) -> InterceptedService - where - F: tocin::service::Interceptor, - { - InterceptedService::new(Self::new(inner), interceptor) - } + // Note: with_interceptor is not supported in tocin due to monoio's !Send constraint + // Interceptors would require tower::Service which expects Send futures #configure_compression_methods #configure_max_message_size_methods } - #[async_trait] impl GrpcService for #server_service where T: #server_trait, { - async fn call( - &mut self, - arena: &defiant::Arena, - req: http::Request, - respond: monoio_http::h2::server::SendResponse, - ) -> Result<(), tocin::Status> { + type CallFuture<'arena> = std::pin::Pin> + 'arena>> + where + Self: 'arena; + + fn call<'arena>( + &'arena mut self, + arena: &'arena defiant::Arena, + req: http::Request, + mut respond: h2::server::SendResponse, + ) -> Self::CallFuture<'arena> { + Box::pin(async move { match req.uri().path() { #methods @@ -185,6 +185,7 @@ pub(crate) fn generate_internal( Ok(()) } } + }) } } @@ -235,8 +236,8 @@ fn generate_trait( quote! { #trait_doc #(#trait_attributes)* - #[async_trait] - pub trait #server_trait : std::marker::Send + std::marker::Sync + 'static { + #[async_trait(?Send)] + pub trait #server_trait : 'static { #methods } } @@ -280,7 +281,7 @@ fn generate_trait_methods( (false, false, true) => { quote! { #method_doc - async fn #name(#self_param, request: tocin::Request<#req_message>) + async fn #name<'arena>(#self_param, arena: &'arena defiant::Arena, request: tocin::Request<#req_message>) -> std::result::Result, tocin::Status> { Err(tocin::Status::unimplemented("Not yet implemented")) } @@ -289,14 +290,14 @@ fn generate_trait_methods( (false, false, false) => { quote! { #method_doc - async fn #name(#self_param, request: tocin::Request<#req_message>) + async fn #name<'arena>(#self_param, arena: &'arena defiant::Arena, request: tocin::Request<#req_message>) -> std::result::Result, tocin::Status>; } } (true, false, true) => { quote! { #method_doc - async fn #name(#self_param, request: tocin::Request>) + async fn #name<'arena>(#self_param, arena: &'arena defiant::Arena, request: tocin::Request>) -> std::result::Result, tocin::Status> { Err(tocin::Status::unimplemented("Not yet implemented")) } @@ -305,14 +306,14 @@ fn generate_trait_methods( (true, false, false) => { quote! { #method_doc - async fn #name(#self_param, request: tocin::Request>) + async fn #name<'arena>(#self_param, arena: &'arena defiant::Arena, request: tocin::Request>) -> std::result::Result, tocin::Status>; } } (false, true, true) => { quote! { #method_doc - async fn #name(#self_param, request: tocin::Request<#req_message>) + async fn #name<'arena>(#self_param, arena: &'arena defiant::Arena, request: tocin::Request<#req_message>) -> std::result::Result>, tocin::Status> { Err(tocin::Status::unimplemented("Not yet implemented")) } @@ -327,17 +328,17 @@ fn generate_trait_methods( quote! { #stream_doc - type #stream: tocin::codegen::tokio_stream::Stream> + std::marker::Send + 'static; + type #stream: tocin::codegen::Stream> + 'static; #method_doc - async fn #name(#self_param, request: tocin::Request<#req_message>) + async fn #name<'arena>(#self_param, arena: &'arena defiant::Arena, request: tocin::Request<#req_message>) -> std::result::Result, tocin::Status>; } } (true, true, true) => { quote! { #method_doc - async fn #name(#self_param, request: tocin::Request>) + async fn #name<'arena>(#self_param, arena: &'arena defiant::Arena, request: tocin::Request>) -> std::result::Result>, tocin::Status> { Err(tocin::Status::unimplemented("Not yet implemented")) } @@ -352,10 +353,10 @@ fn generate_trait_methods( quote! { #stream_doc - type #stream: tocin::codegen::tokio_stream::Stream> + std::marker::Send + 'static; + type #stream: tocin::codegen::Stream> + 'static; #method_doc - async fn #name(#self_param, request: tocin::Request>) + async fn #name<'arena>(#self_param, arena: &'arena defiant::Arena, request: tocin::Request>) -> std::result::Result, tocin::Status>; } } @@ -471,14 +472,14 @@ fn generate_unary( #[allow(non_camel_case_types)] struct #service_ident(pub Arc); - impl tocin::server::UnaryService<#request> for #service_ident { + impl<'arena, T: #server_trait> tocin::server::UnaryService<'arena, #request> for #service_ident { type Response = #response; - type Future = BoxFuture, tocin::Status>; + type Future = BoxFuture<'arena, tocin::Response, tocin::Status>; - fn call(&mut self, request: tocin::Request<#request>) -> Self::Future { + fn call(&mut self, arena: &'arena defiant::Arena, request: tocin::Request<#request>) -> Self::Future { let inner = Arc::clone(&self.0); let fut = async move { - ::#method_ident(#inner_arg, request).await + ::#method_ident(#inner_arg, arena, request).await }; Box::pin(fut) } @@ -490,7 +491,7 @@ fn generate_unary( let max_encoding_message_size = self.max_encoding_message_size; let inner = self.inner.clone(); - let mut method = #service_ident(inner); + let method = #service_ident(inner); let codec = #codec_name::default(); let mut grpc = tocin::server::Grpc::new(codec, arena) @@ -534,15 +535,15 @@ fn generate_server_streaming( #[allow(non_camel_case_types)] struct #service_ident(pub Arc); - impl tocin::server::ServerStreamingService<#request> for #service_ident { + impl<'arena, T: #server_trait> tocin::server::ServerStreamingService<'arena, #request> for #service_ident { type Response = #response; #response_stream; - type Future = BoxFuture, tocin::Status>; + type Future = BoxFuture<'arena, tocin::Response, tocin::Status>; - fn call(&mut self, request: tocin::Request<#request>) -> Self::Future { + fn call(&mut self, arena: &'arena defiant::Arena, request: tocin::Request<#request>) -> Self::Future { let inner = Arc::clone(&self.0); let fut = async move { - ::#method_ident(#inner_arg, request).await + ::#method_ident(#inner_arg, arena, request).await }; Box::pin(fut) } @@ -554,7 +555,7 @@ fn generate_server_streaming( let max_encoding_message_size = self.max_encoding_message_size; let inner = self.inner.clone(); - let mut method = #service_ident(inner); + let method = #service_ident(inner); let codec = #codec_name::default(); let mut grpc = tocin::server::Grpc::new(codec, arena) @@ -589,15 +590,15 @@ fn generate_client_streaming( #[allow(non_camel_case_types)] struct #service_ident(pub Arc); - impl tocin::server::ClientStreamingService<#request> for #service_ident + impl<'arena, T: #server_trait> tocin::server::ClientStreamingService<'arena, #request> for #service_ident { type Response = #response; - type Future = BoxFuture, tocin::Status>; + type Future = BoxFuture<'arena, tocin::Response, tocin::Status>; - fn call(&mut self, request: tocin::Request>) -> Self::Future { + fn call(&mut self, arena: &'arena defiant::Arena, request: tocin::Request>) -> Self::Future { let inner = Arc::clone(&self.0); let fut = async move { - ::#method_ident(#inner_arg, request).await + ::#method_ident(#inner_arg, arena, request).await }; Box::pin(fut) } @@ -609,7 +610,7 @@ fn generate_client_streaming( let max_encoding_message_size = self.max_encoding_message_size; let inner = self.inner.clone(); - let mut method = #service_ident(inner); + let method = #service_ident(inner); let codec = #codec_name::default(); let mut grpc = tocin::server::Grpc::new(codec, arena) @@ -653,16 +654,16 @@ fn generate_streaming( #[allow(non_camel_case_types)] struct #service_ident(pub Arc); - impl tocin::server::StreamingService<#request> for #service_ident + impl<'arena, T: #server_trait> tocin::server::StreamingService<'arena, #request> for #service_ident { type Response = #response; #response_stream; - type Future = BoxFuture, tocin::Status>; + type Future = BoxFuture<'arena, tocin::Response, tocin::Status>; - fn call(&mut self, request: tocin::Request>) -> Self::Future { + fn call(&mut self, arena: &'arena defiant::Arena, request: tocin::Request>) -> Self::Future { let inner = Arc::clone(&self.0); let fut = async move { - ::#method_ident(#inner_arg, request).await + ::#method_ident(#inner_arg, arena, request).await }; Box::pin(fut) } @@ -674,7 +675,7 @@ fn generate_streaming( let max_encoding_message_size = self.max_encoding_message_size; let inner = self.inner.clone(); - let mut method = #service_ident(inner); + let method = #service_ident(inner); let codec = #codec_name::default(); let mut grpc = tocin::server::Grpc::new(codec, arena) diff --git a/tocin-defiant-build/Cargo.toml b/tocin-defiant-build/Cargo.toml index e49347c..5d47250 100644 --- a/tocin-defiant-build/Cargo.toml +++ b/tocin-defiant-build/Cargo.toml @@ -19,8 +19,8 @@ cleanup-markdown = ["defiant-build/cleanup-markdown"] [dependencies] tocin-build = { version = "0.14.0", path = "../tocin-build", default-features = false } defiant-build = { path = "../../defiant/defiant-build" } -defiant-types = { path = "../../defiant/defiant-types" } defiant = { path = "../../defiant/defiant" } +defiant-types = { path = "../../defiant/defiant-types" } prettyplease = { version = "0.2" } proc-macro2 = "1.0" quote = "1.0" diff --git a/tocin-defiant-build/src/lib.rs b/tocin-defiant-build/src/lib.rs index 0b96892..a936ad7 100644 --- a/tocin-defiant-build/src/lib.rs +++ b/tocin-defiant-build/src/lib.rs @@ -222,15 +222,26 @@ impl<'arena> tocin_build::Method for TonicBuildMethod<'arena> { || self.prost_method.input_type.starts_with("crate::") { // This is an extern type, use it directly - self.prost_method.input_type.parse::().unwrap() + if self.prost_method.input_has_arena { + let type_with_lifetime = format!("{}<'arena>", self.prost_method.input_type); + type_with_lifetime.parse::().unwrap() + } else { + self.prost_method.input_type.parse::().unwrap() + } } else { // Replace dots with double colons for the type name let rust_type = self.prost_method.input_type.replace('.', "::"); // Remove leading :: if present let rust_type = rust_type.trim_start_matches("::"); - syn::parse_str::(&format!("{proto_path}::{rust_type}")) - .unwrap() - .to_token_stream() + if self.prost_method.input_has_arena { + syn::parse_str::(&format!("{proto_path}::{rust_type}<'arena>")) + .unwrap() + .to_token_stream() + } else { + syn::parse_str::(&format!("{proto_path}::{rust_type}")) + .unwrap() + .to_token_stream() + } } }; @@ -267,18 +278,26 @@ impl<'arena> tocin_build::Method for TonicBuildMethod<'arena> { || self.prost_method.output_type.starts_with("crate::") { // This is an extern type, use it directly - self.prost_method - .output_type - .parse::() - .unwrap() + if self.prost_method.output_has_arena { + let type_with_lifetime = format!("{}<'arena>", self.prost_method.output_type); + type_with_lifetime.parse::().unwrap() + } else { + self.prost_method.output_type.parse::().unwrap() + } } else { // Replace dots with double colons for the type name let rust_type = self.prost_method.output_type.replace('.', "::"); // Remove leading :: if present let rust_type = rust_type.trim_start_matches("::"); - syn::parse_str::(&format!("{proto_path}::{rust_type}")) - .unwrap() - .to_token_stream() + if self.prost_method.output_has_arena { + syn::parse_str::(&format!("{proto_path}::{rust_type}<'arena>")) + .unwrap() + .to_token_stream() + } else { + syn::parse_str::(&format!("{proto_path}::{rust_type}")) + .unwrap() + .to_token_stream() + } } }; diff --git a/tocin-defiant-build/src/tests.rs b/tocin-defiant-build/src/tests.rs index f9b19f1..94b9e71 100644 --- a/tocin-defiant-build/src/tests.rs +++ b/tocin-defiant-build/src/tests.rs @@ -7,8 +7,6 @@ fn create_test_method<'arena>( input_type: String, output_type: String, ) -> TonicBuildMethod<'arena> { - use defiant::Message; - // Build MethodOptions using defiant builder (this is a protobuf message) let options = defiant_types::MethodOptions::builder(arena).freeze(); @@ -25,6 +23,8 @@ fn create_test_method<'arena>( output_type: output_type.clone(), input_proto_type: input_type, output_proto_type: output_type, + input_has_arena: false, + output_has_arena: false, client_streaming: false, server_streaming: false, options, diff --git a/tocin-defiant/src/codec.rs b/tocin-defiant/src/codec.rs index 0d0fff8..22cd262 100644 --- a/tocin-defiant/src/codec.rs +++ b/tocin-defiant/src/codec.rs @@ -29,10 +29,7 @@ where { /// A tool for building custom codecs based on prost encoding and decoding. /// See the codec_buffers example for one possible way to use this. - pub fn raw_encoder<'arena>(buffer_settings: BufferSettings) -> >::Encoder - where - U: Decode<'arena>, - { + pub fn raw_encoder(buffer_settings: BufferSettings) -> ProstEncoder { ProstEncoder { _pd: PhantomData, buffer_settings, @@ -41,9 +38,9 @@ where /// A tool for building custom codecs based on prost encoding and decoding. /// See the codec_buffers example for one possible way to use this. - pub fn raw_decoder<'arena>(buffer_settings: BufferSettings) -> >::Decoder + pub fn raw_decoder<'arena>(buffer_settings: BufferSettings) -> ProstDecoder where - U: Decode<'arena>, + U: defiant::MessageView<'arena>, { ProstDecoder { _pd: PhantomData, @@ -55,7 +52,7 @@ where impl<'arena, T, U> Codec<'arena> for ProstCodec where T: Encode, - U: Decode<'arena>, + U: defiant::MessageView<'arena> + 'arena, { type Encode = T; type Decode = U; @@ -128,7 +125,7 @@ impl ProstDecoder { } } -impl<'arena, U: Decode<'arena>> Decoder<'arena> for ProstDecoder { +impl<'arena, U: defiant::MessageView<'arena>> Decoder<'arena> for ProstDecoder { type Item = U; type Error = Status; @@ -137,8 +134,8 @@ impl<'arena, U: Decode<'arena>> Decoder<'arena> for ProstDecoder { buf: &mut DecodeBuf<'_>, arena: &'arena Arena, ) -> Result, Self::Error> { - // Decode using the provided arena (passed from connection handler) - let item = Decode::decode(buf, arena) + // Decode using MessageView::from_buf which returns the View directly + let item = U::from_buf(buf, arena) .map(Option::Some) .map_err(from_decode_error)?; diff --git a/tocin-health/Cargo.toml b/tocin-health/Cargo.toml index 92a5053..a529794 100644 --- a/tocin-health/Cargo.toml +++ b/tocin-health/Cargo.toml @@ -15,14 +15,19 @@ version = "0.14.1" rust-version = { workspace = true } [dependencies] -prost = "0.14" -tokio = {version = "1.0", features = ["sync"]} -tokio-stream = {version = "0.1", default-features = false, features = ["sync"]} +defiant = { path = "../../defiant/defiant" } +monoio = { workspace = true } +futures-core = "0.3" +flume = "0.11" +parking_lot = "0.12" tocin = { version = "0.14.0", path = "../tocin", default-features = false, features = ["codegen"] } tocin-defiant = { version = "0.14.0", path = "../tocin-defiant", default-features = false } +[build-dependencies] +tocin-defiant-build = { version = "0.14.0", path = "../tocin-defiant-build" } + [dev-dependencies] -tokio = {version = "1.0", features = ["rt-multi-thread", "macros"]} +futures-util = "0.3" prost-types = "0.14.0" [lints] diff --git a/tocin-health/build.rs b/tocin-health/build.rs new file mode 100644 index 0000000..64f97d0 --- /dev/null +++ b/tocin-health/build.rs @@ -0,0 +1,8 @@ +fn main() { + tocin_defiant_build::configure() + .build_client(false) // Skip client for now + .out_dir("src/generated") + .include_file("mod.rs") + .compile_protos(&["proto/health.proto"], &["proto"]) + .unwrap(); +} diff --git a/tocin-health/src/generated/grpc.health.v1.rs b/tocin-health/src/generated/grpc.health.v1.rs new file mode 100644 index 0000000..b36d23b --- /dev/null +++ b/tocin-health/src/generated/grpc.health.v1.rs @@ -0,0 +1,309 @@ +// This file is @generated by prost-build. +#[derive(Clone, PartialEq, Eq, Hash, ::defiant::View)] +pub struct HealthCheckRequest<'arena> { + #[defiant(string, tag = "1")] + pub service: &'arena str, +} +#[derive(Clone, Copy, PartialEq, Eq, Hash, ::defiant::View)] +pub struct HealthCheckResponse { + #[defiant(enumeration = "health_check_response::ServingStatus", tag = "1")] + pub status: i32, +} +/// Nested message and enum types in `HealthCheckResponse`. +pub mod health_check_response { + #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::defiant::Enumeration)] + #[repr(i32)] + pub enum ServingStatus { + Unknown = 0, + Serving = 1, + NotServing = 2, + /// Used only by the Watch method. + ServiceUnknown = 3, + } + impl ServingStatus { + /// String value of the enum field names used in the ProtoBuf definition. + /// + /// The values are not transformed in any way and thus are considered stable + /// (if the ProtoBuf definition does not change) and safe for programmatic use. + pub fn as_str_name(&self) -> &'static str { + match self { + Self::Unknown => "UNKNOWN", + Self::Serving => "SERVING", + Self::NotServing => "NOT_SERVING", + Self::ServiceUnknown => "SERVICE_UNKNOWN", + } + } + /// Creates an enum from field names used in the ProtoBuf definition. + pub fn from_str_name(value: &str) -> ::core::option::Option { + match value { + "UNKNOWN" => Some(Self::Unknown), + "SERVING" => Some(Self::Serving), + "NOT_SERVING" => Some(Self::NotServing), + "SERVICE_UNKNOWN" => Some(Self::ServiceUnknown), + _ => None, + } + } + } +} +/// Generated server implementations. +pub mod health_server { + #![allow( + unused_variables, + dead_code, + missing_docs, + clippy::wildcard_imports, + clippy::let_unit_value, + )] + use tocin::codegen::*; + /// Generated trait containing gRPC methods that should be implemented for use with HealthServer. + #[async_trait(?Send)] + pub trait Health: 'static { + /// If the requested service is unknown, the call will fail with status + /// NOT_FOUND. + async fn check<'arena>( + &self, + arena: &'arena defiant::Arena, + request: tocin::Request>, + ) -> std::result::Result< + tocin::Response, + tocin::Status, + >; + /// Server streaming response type for the Watch method. + type WatchStream: tocin::codegen::Stream< + Item = std::result::Result, + > + + 'static; + /// Performs a watch for the serving status of the requested service. + /// The server will immediately send back a message indicating the current + /// serving status. It will then subsequently send a new message whenever + /// the service's serving status changes. + /// + /// If the requested service is unknown when the call is received, the + /// server will send a message setting the serving status to + /// SERVICE_UNKNOWN but will *not* terminate the call. If at some + /// future point, the serving status of the service becomes known, the + /// server will send a new message with the service's serving status. + /// + /// If the call terminates with status UNIMPLEMENTED, then clients + /// should assume this method is not supported and should not retry the + /// call. If the call terminates with any other status (including OK), + /// clients should retry the call with appropriate exponential backoff. + async fn watch<'arena>( + &self, + arena: &'arena defiant::Arena, + request: tocin::Request>, + ) -> std::result::Result, tocin::Status>; + } + #[derive(Debug)] + pub struct HealthServer { + inner: Arc, + accept_compression_encodings: EnabledCompressionEncodings, + send_compression_encodings: EnabledCompressionEncodings, + max_decoding_message_size: Option, + max_encoding_message_size: Option, + } + impl HealthServer { + pub fn new(inner: T) -> Self { + Self::from_arc(Arc::new(inner)) + } + pub fn from_arc(inner: Arc) -> Self { + Self { + inner, + accept_compression_encodings: Default::default(), + send_compression_encodings: Default::default(), + max_decoding_message_size: None, + max_encoding_message_size: None, + } + } + /// Enable decompressing requests with the given encoding. + #[must_use] + pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.accept_compression_encodings.enable(encoding); + self + } + /// Compress responses with the given encoding, if the client supports it. + #[must_use] + pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.send_compression_encodings.enable(encoding); + self + } + /// Limits the maximum size of a decoded message. + /// + /// Default: `4MB` + #[must_use] + pub fn max_decoding_message_size(mut self, limit: usize) -> Self { + self.max_decoding_message_size = Some(limit); + self + } + /// Limits the maximum size of an encoded message. + /// + /// Default: `usize::MAX` + #[must_use] + pub fn max_encoding_message_size(mut self, limit: usize) -> Self { + self.max_encoding_message_size = Some(limit); + self + } + } + impl GrpcService for HealthServer + where + T: Health, + { + type CallFuture<'arena> = std::pin::Pin< + Box> + 'arena>, + > + where + Self: 'arena; + fn call<'arena>( + &'arena mut self, + arena: &'arena defiant::Arena, + req: http::Request, + mut respond: h2::server::SendResponse, + ) -> Self::CallFuture<'arena> { + Box::pin(async move { + match req.uri().path() { + "/grpc.health.v1.Health/Check" => { + #[allow(non_camel_case_types)] + struct CheckSvc(pub Arc); + impl< + 'arena, + T: Health, + > tocin::server::UnaryService< + 'arena, + super::HealthCheckRequest<'arena>, + > for CheckSvc { + type Response = super::HealthCheckResponse; + type Future = BoxFuture< + 'arena, + tocin::Response, + tocin::Status, + >; + fn call( + &mut self, + arena: &'arena defiant::Arena, + request: tocin::Request>, + ) -> Self::Future { + let inner = Arc::clone(&self.0); + let fut = async move { + ::check(&inner, arena, request).await + }; + Box::pin(fut) + } + } + let accept_compression_encodings = self + .accept_compression_encodings; + let send_compression_encodings = self.send_compression_encodings; + let max_decoding_message_size = self.max_decoding_message_size; + let max_encoding_message_size = self.max_encoding_message_size; + let inner = self.inner.clone(); + let method = CheckSvc(inner); + let codec = tocin_defiant::ProstCodec::default(); + let mut grpc = tocin::server::Grpc::new(codec, arena) + .apply_compression_config( + accept_compression_encodings, + send_compression_encodings, + ) + .apply_max_message_size_config( + max_decoding_message_size, + max_encoding_message_size, + ); + grpc.unary(method, req, respond).await?; + Ok(()) + } + "/grpc.health.v1.Health/Watch" => { + #[allow(non_camel_case_types)] + struct WatchSvc(pub Arc); + impl< + 'arena, + T: Health, + > tocin::server::ServerStreamingService< + 'arena, + super::HealthCheckRequest<'arena>, + > for WatchSvc { + type Response = super::HealthCheckResponse; + type ResponseStream = T::WatchStream; + type Future = BoxFuture< + 'arena, + tocin::Response, + tocin::Status, + >; + fn call( + &mut self, + arena: &'arena defiant::Arena, + request: tocin::Request>, + ) -> Self::Future { + let inner = Arc::clone(&self.0); + let fut = async move { + ::watch(&inner, arena, request).await + }; + Box::pin(fut) + } + } + let accept_compression_encodings = self + .accept_compression_encodings; + let send_compression_encodings = self.send_compression_encodings; + let max_decoding_message_size = self.max_decoding_message_size; + let max_encoding_message_size = self.max_encoding_message_size; + let inner = self.inner.clone(); + let method = WatchSvc(inner); + let codec = tocin_defiant::ProstCodec::default(); + let mut grpc = tocin::server::Grpc::new(codec, arena) + .apply_compression_config( + accept_compression_encodings, + send_compression_encodings, + ) + .apply_max_message_size_config( + max_decoding_message_size, + max_encoding_message_size, + ); + grpc.server_streaming(method, req, respond).await?; + Ok(()) + } + _ => { + let status = tocin::Status::unimplemented( + format!("Unknown method {}", req.uri().path()), + ); + let response = http::Response::builder() + .status(http::StatusCode::OK) + .header( + http::header::CONTENT_TYPE, + tocin::metadata::GRPC_CONTENT_TYPE, + ) + .body(()) + .map_err(|e| tocin::Status::internal( + format!("Failed to build error response: {}", e), + ))?; + let mut send_stream = respond + .send_response(response, false) + .map_err(|e| tocin::Status::internal( + format!("h2 error: {}", e), + ))?; + let trailers = status.to_header_map()?; + send_stream + .send_trailers(trailers) + .map_err(|e| tocin::Status::internal( + format!("h2 error: {}", e), + ))?; + Ok(()) + } + } + }) + } + } + impl Clone for HealthServer { + fn clone(&self) -> Self { + let inner = self.inner.clone(); + Self { + inner, + accept_compression_encodings: self.accept_compression_encodings, + send_compression_encodings: self.send_compression_encodings, + max_decoding_message_size: self.max_decoding_message_size, + max_encoding_message_size: self.max_encoding_message_size, + } + } + } + /// Generated gRPC service name + pub const SERVICE_NAME: &str = "grpc.health.v1.Health"; + impl tocin::server::NamedService for HealthServer { + const NAME: &'static str = SERVICE_NAME; + } +} diff --git a/tocin-health/src/generated/grpc_health_v1.rs b/tocin-health/src/generated/grpc_health_v1.rs deleted file mode 100644 index 85add48..0000000 --- a/tocin-health/src/generated/grpc_health_v1.rs +++ /dev/null @@ -1,459 +0,0 @@ -// This file is @generated by prost-build. -#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] -pub struct HealthCheckRequest { - #[prost(string, tag = "1")] - pub service: ::prost::alloc::string::String, -} -#[derive(Clone, Copy, PartialEq, Eq, Hash, ::prost::Message)] -pub struct HealthCheckResponse { - #[prost(enumeration = "health_check_response::ServingStatus", tag = "1")] - pub status: i32, -} -/// Nested message and enum types in `HealthCheckResponse`. -pub mod health_check_response { - #[derive( - Clone, - Copy, - Debug, - PartialEq, - Eq, - Hash, - PartialOrd, - Ord, - ::prost::Enumeration - )] - #[repr(i32)] - pub enum ServingStatus { - Unknown = 0, - Serving = 1, - NotServing = 2, - /// Used only by the Watch method. - ServiceUnknown = 3, - } - impl ServingStatus { - /// String value of the enum field names used in the ProtoBuf definition. - /// - /// The values are not transformed in any way and thus are considered stable - /// (if the ProtoBuf definition does not change) and safe for programmatic use. - pub fn as_str_name(&self) -> &'static str { - match self { - Self::Unknown => "UNKNOWN", - Self::Serving => "SERVING", - Self::NotServing => "NOT_SERVING", - Self::ServiceUnknown => "SERVICE_UNKNOWN", - } - } - /// Creates an enum from field names used in the ProtoBuf definition. - pub fn from_str_name(value: &str) -> ::core::option::Option { - match value { - "UNKNOWN" => Some(Self::Unknown), - "SERVING" => Some(Self::Serving), - "NOT_SERVING" => Some(Self::NotServing), - "SERVICE_UNKNOWN" => Some(Self::ServiceUnknown), - _ => None, - } - } - } -} -/// Generated client implementations. -pub mod health_client { - #![allow( - unused_variables, - dead_code, - missing_docs, - clippy::wildcard_imports, - clippy::let_unit_value, - )] - use tocin::codegen::*; - use tocin::codegen::http::Uri; - #[derive(Debug, Clone)] - pub struct HealthClient { - inner: tocin::client::Grpc, - } - impl HealthClient - where - T: tocin::client::GrpcService, - T::Error: Into, - T::ResponseBody: Body + std::marker::Send + 'static, - ::Error: Into + std::marker::Send, - { - pub fn new(inner: T) -> Self { - let inner = tocin::client::Grpc::new(inner); - Self { inner } - } - pub fn with_origin(inner: T, origin: Uri) -> Self { - let inner = tocin::client::Grpc::with_origin(inner, origin); - Self { inner } - } - pub fn with_interceptor( - inner: T, - interceptor: F, - ) -> HealthClient> - where - F: tocin::service::Interceptor, - T::ResponseBody: Default, - T: tocin::codegen::Service< - http::Request, - Response = http::Response< - >::ResponseBody, - >, - >, - , - >>::Error: Into + std::marker::Send + std::marker::Sync, - { - HealthClient::new(InterceptedService::new(inner, interceptor)) - } - /// Compress requests with the given encoding. - /// - /// This requires the server to support it otherwise it might respond with an - /// error. - #[must_use] - pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self { - self.inner = self.inner.send_compressed(encoding); - self - } - /// Enable decompressing responses. - #[must_use] - pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self { - self.inner = self.inner.accept_compressed(encoding); - self - } - /// Limits the maximum size of a decoded message. - /// - /// Default: `4MB` - #[must_use] - pub fn max_decoding_message_size(mut self, limit: usize) -> Self { - self.inner = self.inner.max_decoding_message_size(limit); - self - } - /// Limits the maximum size of an encoded message. - /// - /// Default: `usize::MAX` - #[must_use] - pub fn max_encoding_message_size(mut self, limit: usize) -> Self { - self.inner = self.inner.max_encoding_message_size(limit); - self - } - /// If the requested service is unknown, the call will fail with status - /// NOT_FOUND. - pub async fn check( - &mut self, - request: impl tocin::IntoRequest, - ) -> std::result::Result< - tocin::Response, - tocin::Status, - > { - self.inner - .ready() - .await - .map_err(|e| { - tocin::Status::unknown( - format!("Service was not ready: {}", e.into()), - ) - })?; - let codec = tocin_defiant::ProstCodec::default(); - let path = http::uri::PathAndQuery::from_static( - "/grpc.health.v1.Health/Check", - ); - let mut req = request.into_request(); - req.extensions_mut() - .insert(GrpcMethod::new("grpc.health.v1.Health", "Check")); - self.inner.unary(req, path, codec).await - } - /// Performs a watch for the serving status of the requested service. - /// The server will immediately send back a message indicating the current - /// serving status. It will then subsequently send a new message whenever - /// the service's serving status changes. - /// - /// If the requested service is unknown when the call is received, the - /// server will send a message setting the serving status to - /// SERVICE_UNKNOWN but will *not* terminate the call. If at some - /// future point, the serving status of the service becomes known, the - /// server will send a new message with the service's serving status. - /// - /// If the call terminates with status UNIMPLEMENTED, then clients - /// should assume this method is not supported and should not retry the - /// call. If the call terminates with any other status (including OK), - /// clients should retry the call with appropriate exponential backoff. - pub async fn watch( - &mut self, - request: impl tocin::IntoRequest, - ) -> std::result::Result< - tocin::Response>, - tocin::Status, - > { - self.inner - .ready() - .await - .map_err(|e| { - tocin::Status::unknown( - format!("Service was not ready: {}", e.into()), - ) - })?; - let codec = tocin_defiant::ProstCodec::default(); - let path = http::uri::PathAndQuery::from_static( - "/grpc.health.v1.Health/Watch", - ); - let mut req = request.into_request(); - req.extensions_mut() - .insert(GrpcMethod::new("grpc.health.v1.Health", "Watch")); - self.inner.server_streaming(req, path, codec).await - } - } -} -/// Generated server implementations. -pub mod health_server { - #![allow( - unused_variables, - dead_code, - missing_docs, - clippy::wildcard_imports, - clippy::let_unit_value, - )] - use tocin::codegen::*; - /// Generated trait containing gRPC methods that should be implemented for use with HealthServer. - #[async_trait] - pub trait Health: std::marker::Send + std::marker::Sync + 'static { - /// If the requested service is unknown, the call will fail with status - /// NOT_FOUND. - async fn check( - &self, - request: tocin::Request, - ) -> std::result::Result< - tocin::Response, - tocin::Status, - >; - /// Server streaming response type for the Watch method. - type WatchStream: tocin::codegen::tokio_stream::Stream< - Item = std::result::Result, - > - + std::marker::Send - + 'static; - /// Performs a watch for the serving status of the requested service. - /// The server will immediately send back a message indicating the current - /// serving status. It will then subsequently send a new message whenever - /// the service's serving status changes. - /// - /// If the requested service is unknown when the call is received, the - /// server will send a message setting the serving status to - /// SERVICE_UNKNOWN but will *not* terminate the call. If at some - /// future point, the serving status of the service becomes known, the - /// server will send a new message with the service's serving status. - /// - /// If the call terminates with status UNIMPLEMENTED, then clients - /// should assume this method is not supported and should not retry the - /// call. If the call terminates with any other status (including OK), - /// clients should retry the call with appropriate exponential backoff. - async fn watch( - &self, - request: tocin::Request, - ) -> std::result::Result, tocin::Status>; - } - #[derive(Debug)] - pub struct HealthServer { - inner: Arc, - accept_compression_encodings: EnabledCompressionEncodings, - send_compression_encodings: EnabledCompressionEncodings, - max_decoding_message_size: Option, - max_encoding_message_size: Option, - } - impl HealthServer { - pub fn new(inner: T) -> Self { - Self::from_arc(Arc::new(inner)) - } - pub fn from_arc(inner: Arc) -> Self { - Self { - inner, - accept_compression_encodings: Default::default(), - send_compression_encodings: Default::default(), - max_decoding_message_size: None, - max_encoding_message_size: None, - } - } - pub fn with_interceptor( - inner: T, - interceptor: F, - ) -> InterceptedService - where - F: tocin::service::Interceptor, - { - InterceptedService::new(Self::new(inner), interceptor) - } - /// Enable decompressing requests with the given encoding. - #[must_use] - pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self { - self.accept_compression_encodings.enable(encoding); - self - } - /// Compress responses with the given encoding, if the client supports it. - #[must_use] - pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self { - self.send_compression_encodings.enable(encoding); - self - } - /// Limits the maximum size of a decoded message. - /// - /// Default: `4MB` - #[must_use] - pub fn max_decoding_message_size(mut self, limit: usize) -> Self { - self.max_decoding_message_size = Some(limit); - self - } - /// Limits the maximum size of an encoded message. - /// - /// Default: `usize::MAX` - #[must_use] - pub fn max_encoding_message_size(mut self, limit: usize) -> Self { - self.max_encoding_message_size = Some(limit); - self - } - } - impl tocin::codegen::Service> for HealthServer - where - T: Health, - B: Body + std::marker::Send + 'static, - B::Error: Into + std::marker::Send + 'static, - { - type Response = http::Response; - type Error = std::convert::Infallible; - type Future = BoxFuture; - fn poll_ready( - &mut self, - _cx: &mut Context<'_>, - ) -> Poll> { - Poll::Ready(Ok(())) - } - fn call(&mut self, req: http::Request) -> Self::Future { - match req.uri().path() { - "/grpc.health.v1.Health/Check" => { - #[allow(non_camel_case_types)] - struct CheckSvc(pub Arc); - impl< - T: Health, - > tocin::server::UnaryService - for CheckSvc { - type Response = super::HealthCheckResponse; - type Future = BoxFuture< - tocin::Response, - tocin::Status, - >; - fn call( - &mut self, - request: tocin::Request, - ) -> Self::Future { - let inner = Arc::clone(&self.0); - let fut = async move { - ::check(&inner, request).await - }; - Box::pin(fut) - } - } - let accept_compression_encodings = self.accept_compression_encodings; - let send_compression_encodings = self.send_compression_encodings; - let max_decoding_message_size = self.max_decoding_message_size; - let max_encoding_message_size = self.max_encoding_message_size; - let inner = self.inner.clone(); - let fut = async move { - let method = CheckSvc(inner); - let codec = tocin_defiant::ProstCodec::default(); - let mut grpc = tocin::server::Grpc::new(codec) - .apply_compression_config( - accept_compression_encodings, - send_compression_encodings, - ) - .apply_max_message_size_config( - max_decoding_message_size, - max_encoding_message_size, - ); - let res = grpc.unary(method, req).await; - Ok(res) - }; - Box::pin(fut) - } - "/grpc.health.v1.Health/Watch" => { - #[allow(non_camel_case_types)] - struct WatchSvc(pub Arc); - impl< - T: Health, - > tocin::server::ServerStreamingService - for WatchSvc { - type Response = super::HealthCheckResponse; - type ResponseStream = T::WatchStream; - type Future = BoxFuture< - tocin::Response, - tocin::Status, - >; - fn call( - &mut self, - request: tocin::Request, - ) -> Self::Future { - let inner = Arc::clone(&self.0); - let fut = async move { - ::watch(&inner, request).await - }; - Box::pin(fut) - } - } - let accept_compression_encodings = self.accept_compression_encodings; - let send_compression_encodings = self.send_compression_encodings; - let max_decoding_message_size = self.max_decoding_message_size; - let max_encoding_message_size = self.max_encoding_message_size; - let inner = self.inner.clone(); - let fut = async move { - let method = WatchSvc(inner); - let codec = tocin_defiant::ProstCodec::default(); - let mut grpc = tocin::server::Grpc::new(codec) - .apply_compression_config( - accept_compression_encodings, - send_compression_encodings, - ) - .apply_max_message_size_config( - max_decoding_message_size, - max_encoding_message_size, - ); - let res = grpc.server_streaming(method, req).await; - Ok(res) - }; - Box::pin(fut) - } - _ => { - Box::pin(async move { - let mut response = http::Response::new( - tocin::body::Body::default(), - ); - let headers = response.headers_mut(); - headers - .insert( - tocin::Status::GRPC_STATUS, - (tocin::Code::Unimplemented as i32).into(), - ); - headers - .insert( - http::header::CONTENT_TYPE, - tocin::metadata::GRPC_CONTENT_TYPE, - ); - Ok(response) - }) - } - } - } - } - impl Clone for HealthServer { - fn clone(&self) -> Self { - let inner = self.inner.clone(); - Self { - inner, - accept_compression_encodings: self.accept_compression_encodings, - send_compression_encodings: self.send_compression_encodings, - max_decoding_message_size: self.max_decoding_message_size, - max_encoding_message_size: self.max_encoding_message_size, - } - } - } - /// Generated gRPC service name - pub const SERVICE_NAME: &str = "grpc.health.v1.Health"; - impl tocin::server::NamedService for HealthServer { - const NAME: &'static str = SERVICE_NAME; - } -} diff --git a/tocin-health/src/generated/grpc_health_v1_fds.rs b/tocin-health/src/generated/grpc_health_v1_fds.rs deleted file mode 100644 index 45cc00b..0000000 --- a/tocin-health/src/generated/grpc_health_v1_fds.rs +++ /dev/null @@ -1,63 +0,0 @@ -// This file is @generated by codegen. -// Copyright 2015 The gRPC Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// The canonical version of this proto can be found at -// https://github.com/grpc/grpc-proto/blob/master/grpc/health/v1/health.proto -// -/// Byte encoded FILE_DESCRIPTOR_SET. -pub const FILE_DESCRIPTOR_SET: &[u8] = &[ - 10u8, 158u8, 4u8, 10u8, 12u8, 104u8, 101u8, 97u8, 108u8, 116u8, 104u8, 46u8, 112u8, - 114u8, 111u8, 116u8, 111u8, 18u8, 14u8, 103u8, 114u8, 112u8, 99u8, 46u8, 104u8, - 101u8, 97u8, 108u8, 116u8, 104u8, 46u8, 118u8, 49u8, 34u8, 46u8, 10u8, 18u8, 72u8, - 101u8, 97u8, 108u8, 116u8, 104u8, 67u8, 104u8, 101u8, 99u8, 107u8, 82u8, 101u8, - 113u8, 117u8, 101u8, 115u8, 116u8, 18u8, 24u8, 10u8, 7u8, 115u8, 101u8, 114u8, 118u8, - 105u8, 99u8, 101u8, 24u8, 1u8, 32u8, 1u8, 40u8, 9u8, 82u8, 7u8, 115u8, 101u8, 114u8, - 118u8, 105u8, 99u8, 101u8, 34u8, 177u8, 1u8, 10u8, 19u8, 72u8, 101u8, 97u8, 108u8, - 116u8, 104u8, 67u8, 104u8, 101u8, 99u8, 107u8, 82u8, 101u8, 115u8, 112u8, 111u8, - 110u8, 115u8, 101u8, 18u8, 73u8, 10u8, 6u8, 115u8, 116u8, 97u8, 116u8, 117u8, 115u8, - 24u8, 1u8, 32u8, 1u8, 40u8, 14u8, 50u8, 49u8, 46u8, 103u8, 114u8, 112u8, 99u8, 46u8, - 104u8, 101u8, 97u8, 108u8, 116u8, 104u8, 46u8, 118u8, 49u8, 46u8, 72u8, 101u8, 97u8, - 108u8, 116u8, 104u8, 67u8, 104u8, 101u8, 99u8, 107u8, 82u8, 101u8, 115u8, 112u8, - 111u8, 110u8, 115u8, 101u8, 46u8, 83u8, 101u8, 114u8, 118u8, 105u8, 110u8, 103u8, - 83u8, 116u8, 97u8, 116u8, 117u8, 115u8, 82u8, 6u8, 115u8, 116u8, 97u8, 116u8, 117u8, - 115u8, 34u8, 79u8, 10u8, 13u8, 83u8, 101u8, 114u8, 118u8, 105u8, 110u8, 103u8, 83u8, - 116u8, 97u8, 116u8, 117u8, 115u8, 18u8, 11u8, 10u8, 7u8, 85u8, 78u8, 75u8, 78u8, - 79u8, 87u8, 78u8, 16u8, 0u8, 18u8, 11u8, 10u8, 7u8, 83u8, 69u8, 82u8, 86u8, 73u8, - 78u8, 71u8, 16u8, 1u8, 18u8, 15u8, 10u8, 11u8, 78u8, 79u8, 84u8, 95u8, 83u8, 69u8, - 82u8, 86u8, 73u8, 78u8, 71u8, 16u8, 2u8, 18u8, 19u8, 10u8, 15u8, 83u8, 69u8, 82u8, - 86u8, 73u8, 67u8, 69u8, 95u8, 85u8, 78u8, 75u8, 78u8, 79u8, 87u8, 78u8, 16u8, 3u8, - 50u8, 174u8, 1u8, 10u8, 6u8, 72u8, 101u8, 97u8, 108u8, 116u8, 104u8, 18u8, 80u8, - 10u8, 5u8, 67u8, 104u8, 101u8, 99u8, 107u8, 18u8, 34u8, 46u8, 103u8, 114u8, 112u8, - 99u8, 46u8, 104u8, 101u8, 97u8, 108u8, 116u8, 104u8, 46u8, 118u8, 49u8, 46u8, 72u8, - 101u8, 97u8, 108u8, 116u8, 104u8, 67u8, 104u8, 101u8, 99u8, 107u8, 82u8, 101u8, - 113u8, 117u8, 101u8, 115u8, 116u8, 26u8, 35u8, 46u8, 103u8, 114u8, 112u8, 99u8, 46u8, - 104u8, 101u8, 97u8, 108u8, 116u8, 104u8, 46u8, 118u8, 49u8, 46u8, 72u8, 101u8, 97u8, - 108u8, 116u8, 104u8, 67u8, 104u8, 101u8, 99u8, 107u8, 82u8, 101u8, 115u8, 112u8, - 111u8, 110u8, 115u8, 101u8, 18u8, 82u8, 10u8, 5u8, 87u8, 97u8, 116u8, 99u8, 104u8, - 18u8, 34u8, 46u8, 103u8, 114u8, 112u8, 99u8, 46u8, 104u8, 101u8, 97u8, 108u8, 116u8, - 104u8, 46u8, 118u8, 49u8, 46u8, 72u8, 101u8, 97u8, 108u8, 116u8, 104u8, 67u8, 104u8, - 101u8, 99u8, 107u8, 82u8, 101u8, 113u8, 117u8, 101u8, 115u8, 116u8, 26u8, 35u8, 46u8, - 103u8, 114u8, 112u8, 99u8, 46u8, 104u8, 101u8, 97u8, 108u8, 116u8, 104u8, 46u8, - 118u8, 49u8, 46u8, 72u8, 101u8, 97u8, 108u8, 116u8, 104u8, 67u8, 104u8, 101u8, 99u8, - 107u8, 82u8, 101u8, 115u8, 112u8, 111u8, 110u8, 115u8, 101u8, 48u8, 1u8, 66u8, 97u8, - 10u8, 17u8, 105u8, 111u8, 46u8, 103u8, 114u8, 112u8, 99u8, 46u8, 104u8, 101u8, 97u8, - 108u8, 116u8, 104u8, 46u8, 118u8, 49u8, 66u8, 11u8, 72u8, 101u8, 97u8, 108u8, 116u8, - 104u8, 80u8, 114u8, 111u8, 116u8, 111u8, 80u8, 1u8, 90u8, 44u8, 103u8, 111u8, 111u8, - 103u8, 108u8, 101u8, 46u8, 103u8, 111u8, 108u8, 97u8, 110u8, 103u8, 46u8, 111u8, - 114u8, 103u8, 47u8, 103u8, 114u8, 112u8, 99u8, 47u8, 104u8, 101u8, 97u8, 108u8, - 116u8, 104u8, 47u8, 103u8, 114u8, 112u8, 99u8, 95u8, 104u8, 101u8, 97u8, 108u8, - 116u8, 104u8, 95u8, 118u8, 49u8, 170u8, 2u8, 14u8, 71u8, 114u8, 112u8, 99u8, 46u8, - 72u8, 101u8, 97u8, 108u8, 116u8, 104u8, 46u8, 86u8, 49u8, 98u8, 6u8, 112u8, 114u8, - 111u8, 116u8, 111u8, 51u8, -]; diff --git a/tocin-health/src/generated/mod.rs b/tocin-health/src/generated/mod.rs new file mode 100644 index 0000000..dc21f52 --- /dev/null +++ b/tocin-health/src/generated/mod.rs @@ -0,0 +1,8 @@ +// This file is @generated by prost-build. +pub mod grpc { + pub mod health { + pub mod v1 { + include!("grpc.health.v1.rs"); + } + } +} diff --git a/tocin-health/src/lib.rs b/tocin-health/src/lib.rs index 5884fd8..20ac464 100644 --- a/tocin-health/src/lib.rs +++ b/tocin-health/src/lib.rs @@ -18,28 +18,12 @@ use std::fmt::{Display, Formatter}; mod generated { #![allow(unreachable_pub)] #![allow(missing_docs)] - #[rustfmt::skip] - pub mod grpc_health_v1; - #[rustfmt::skip] - pub mod grpc_health_v1_fds; - - pub use grpc_health_v1_fds::FILE_DESCRIPTOR_SET; - - #[cfg(test)] - mod tests { - use super::FILE_DESCRIPTOR_SET; - use prost::Message as _; - - #[test] - fn file_descriptor_set_is_valid() { - prost_types::FileDescriptorSet::decode(FILE_DESCRIPTOR_SET).unwrap(); - } - } + include!("generated/mod.rs"); } /// Generated protobuf types from the `grpc.health.v1` package. pub mod pb { - pub use crate::generated::{grpc_health_v1::*, FILE_DESCRIPTOR_SET}; + pub use crate::generated::grpc::health::v1::*; } pub mod server; diff --git a/tocin-health/src/server.rs b/tocin-health/src/server.rs index 9250853..c6bd1e3 100644 --- a/tocin-health/src/server.rs +++ b/tocin-health/src/server.rs @@ -6,8 +6,8 @@ use crate::ServingStatus; use std::collections::HashMap; use std::fmt; use std::sync::Arc; -use tokio::sync::{watch, RwLock}; -use tokio_stream::Stream; +use futures_core::Stream; +use parking_lot::RwLock; use tocin::{server::NamedService, Request, Response, Status}; /// Creates a `HealthReporter` and a linked `HealthServer` pair. Together, @@ -15,9 +15,8 @@ use tocin::{server::NamedService, Request, Response, Status}; /// /// A `HealthReporter` is used to update the state of gRPC services. /// -/// A `HealthServer` is a Tonic gRPC server for the `grpc.health.v1.Health`, -/// which can be added to a Tonic runtime using `add_service` on the runtime -/// builder. +/// A `HealthServer` is a Tocin gRPC server for the `grpc.health.v1.Health`, +/// which can be added to a Tocin runtime using `serve` on the server. pub fn health_reporter() -> (HealthReporter, HealthServer) { let reporter = HealthReporter::new(); let service = HealthService::new(reporter.statuses.clone()); @@ -26,75 +25,108 @@ pub fn health_reporter() -> (HealthReporter, HealthServer) { (reporter, server) } -type StatusPair = (watch::Sender, watch::Receiver); +/// A sender/receiver pair for a single service's status. +/// Uses flume for async-compatible change notification on the monoio runtime. +struct StatusEntry { + current: ServingStatus, + /// Senders for active watchers of this service + watchers: Vec>, +} + +impl StatusEntry { + fn new(status: ServingStatus) -> Self { + Self { + current: status, + watchers: Vec::new(), + } + } + + fn set(&mut self, status: ServingStatus) { + self.current = status; + // Notify all watchers, removing any that have been dropped + self.watchers.retain(|tx| tx.send(status).is_ok()); + } + + fn subscribe(&mut self) -> (ServingStatus, flume::Receiver) { + let (tx, rx) = flume::unbounded(); + self.watchers.push(tx); + (self.current, rx) + } +} /// A handle providing methods to update the health status of gRPC services. A /// `HealthReporter` is connected to a `HealthServer` which serves the statuses /// over the `grpc.health.v1.Health` service. #[derive(Clone, Debug)] pub struct HealthReporter { - statuses: Arc>>, + statuses: Arc>>, +} + +impl fmt::Debug for StatusEntry { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("StatusEntry") + .field("current", &self.current) + .field("watchers", &self.watchers.len()) + .finish() + } } impl HealthReporter { /// Create a new HealthReporter with an initial service (named ""), corresponding to overall server health pub fn new() -> Self { // According to the gRPC Health Check specification, the empty service "" corresponds to the overall server health - let server_status = ("".to_string(), watch::channel(ServingStatus::Serving)); + let mut statuses = HashMap::new(); + statuses.insert("".to_string(), StatusEntry::new(ServingStatus::Serving)); - let statuses = Arc::new(RwLock::new(HashMap::from([server_status]))); - - HealthReporter { statuses } + HealthReporter { + statuses: Arc::new(RwLock::new(statuses)), + } } /// Sets the status of the service implemented by `S` to `Serving`. This notifies any watchers /// if there is a change in status. - pub async fn set_serving(&self) + pub fn set_serving(&self) where S: NamedService, { let service_name = ::NAME; - self.set_service_status(service_name, ServingStatus::Serving) - .await; + self.set_service_status(service_name, ServingStatus::Serving); } /// Sets the status of the service implemented by `S` to `NotServing`. This notifies any watchers /// if there is a change in status. - pub async fn set_not_serving(&self) + pub fn set_not_serving(&self) where S: NamedService, { let service_name = ::NAME; - self.set_service_status(service_name, ServingStatus::NotServing) - .await; + self.set_service_status(service_name, ServingStatus::NotServing); } /// Sets the status of the service with `service_name` to `status`. This notifies any watchers /// if there is a change in status. - pub async fn set_service_status(&self, service_name: S, status: ServingStatus) + pub fn set_service_status(&self, service_name: S, status: ServingStatus) where S: AsRef, { let service_name = service_name.as_ref(); - let mut writer = self.statuses.write().await; - match writer.get(service_name) { - Some((tx, _)) => { - // We only ever hand out clones of the receiver, so the originally-created - // receiver should always be present, only being dropped when clearing the - // service status. Consequently, `tx.send` should not fail, making use - // of `expect` here safe. - tx.send(status).expect("channel should not be closed"); + let mut writer = self.statuses.write(); + match writer.get_mut(service_name) { + Some(entry) => { + entry.set(status); } None => { - writer.insert(service_name.to_string(), watch::channel(status)); + writer.insert(service_name.to_string(), StatusEntry::new(status)); } }; } /// Clear the status of the given service. - pub async fn clear_service_status(&mut self, service_name: &str) { - let mut writer = self.statuses.write().await; + pub fn clear_service_status(&mut self, service_name: &str) { + let mut writer = self.statuses.write(); let _ = writer.remove(service_name); + // Dropping the StatusEntry will drop all flume senders, + // causing watcher streams to end } } @@ -107,11 +139,11 @@ impl Default for HealthReporter { /// A service providing implementations of gRPC health checking protocol. #[derive(Debug)] pub struct HealthService { - statuses: Arc>>, + statuses: Arc>>, } impl HealthService { - fn new(services: Arc>>) -> Self { + fn new(services: Arc>>) -> Self { HealthService { statuses: services } } @@ -120,20 +152,21 @@ impl HealthService { Self::new(health_reporter.statuses) } - async fn service_health(&self, service_name: &str) -> Option { - let reader = self.statuses.read().await; - reader.get(service_name).map(|p| *p.1.borrow()) + fn service_health(&self, service_name: &str) -> Option { + let reader = self.statuses.read(); + reader.get(service_name).map(|entry| entry.current) } } -#[tocin::async_trait] +#[tocin::async_trait(?Send)] impl Health for HealthService { - async fn check( + async fn check<'arena>( &self, - request: Request, + _arena: &'arena defiant::Arena, + request: Request>, ) -> Result, Status> { - let service_name = request.get_ref().service.as_str(); - let Some(status) = self.service_health(service_name).await else { + let service_name = request.get_ref().service; + let Some(status) = self.service_health(service_name) else { return Err(Status::not_found("service not registered")); }; @@ -142,29 +175,38 @@ impl Health for HealthService { type WatchStream = WatchStream; - async fn watch( + async fn watch<'arena>( &self, - request: Request, + _arena: &'arena defiant::Arena, + request: Request>, ) -> Result, Status> { - let service_name = request.get_ref().service.as_str(); - let status_rx = match self.statuses.read().await.get(service_name) { - Some((_tx, rx)) => rx.clone(), - None => return Err(Status::not_found("service not registered")), - }; - - Ok(Response::new(WatchStream::new(status_rx))) + let service_name = request.get_ref().service; + let mut writer = self.statuses.write(); + let entry = writer + .get_mut(service_name) + .ok_or_else(|| Status::not_found("service not registered"))?; + let (current, rx) = entry.subscribe(); + + Ok(Response::new(WatchStream::new(current, rx))) } } /// A watch stream for the health service. +/// +/// First yields the current status, then yields new values as the status changes. pub struct WatchStream { - inner: tokio_stream::wrappers::WatchStream, + /// The initial status to yield first + initial: Option, + /// Receiver for subsequent status changes + rx: flume::Receiver, } impl WatchStream { - fn new(status_rx: watch::Receiver) -> Self { - let inner = tokio_stream::wrappers::WatchStream::new(status_rx); - Self { inner } + fn new(current: ServingStatus, rx: flume::Receiver) -> Self { + Self { + initial: Some(current), + rx, + } } } @@ -175,7 +217,15 @@ impl Stream for WatchStream { mut self: std::pin::Pin<&mut Self>, cx: &mut std::task::Context<'_>, ) -> std::task::Poll> { - std::pin::Pin::new(&mut self.inner) + // First yield the initial value + if let Some(status) = self.initial.take() { + return std::task::Poll::Ready(Some(Ok(HealthCheckResponse::new(status)))); + } + + // Then poll the flume receiver for changes + let recv_stream = self.rx.stream(); + let pinned = std::pin::pin!(recv_stream); + pinned .poll_next(cx) .map(|opt| opt.map(|status| Ok(HealthCheckResponse::new(status)))) } @@ -200,8 +250,7 @@ mod tests { use crate::pb::HealthCheckRequest; use crate::server::{HealthReporter, HealthService}; use crate::ServingStatus; - use tokio::sync::watch; - use tokio_stream::StreamExt; + use futures_util::StreamExt; use tocin::{Code, Request, Status}; fn assert_serving_status(wire: i32, expected: ServingStatus) { @@ -214,15 +263,15 @@ mod tests { assert_eq!(wire, expected); } - async fn make_test_service() -> (HealthReporter, HealthService) { + fn make_test_service() -> (HealthReporter, HealthService) { let health_reporter = HealthReporter::new(); // insert test value { - let mut statuses = health_reporter.statuses.write().await; + let mut statuses = health_reporter.statuses.write(); statuses.insert( "TestService".to_string(), - watch::channel(ServingStatus::Unknown), + super::StatusEntry::new(ServingStatus::Unknown), ); } @@ -230,15 +279,17 @@ mod tests { (health_reporter, health_service) } - #[tokio::test] + #[monoio::test_all] async fn test_service_check() { - let (reporter, service) = make_test_service().await; + let arena = defiant::Arena::new(); + let (reporter, service) = make_test_service(); // Overall server health let resp = service - .check(Request::new(HealthCheckRequest { - service: "".to_string(), - })) + .check( + &arena, + Request::new(HealthCheckRequest { service: "" }), + ) .await; assert!(resp.is_ok()); let resp = resp.unwrap().into_inner(); @@ -246,46 +297,55 @@ mod tests { // Unregistered service let resp = service - .check(Request::new(HealthCheckRequest { - service: "Unregistered".to_string(), - })) + .check( + &arena, + Request::new(HealthCheckRequest { + service: "Unregistered", + }), + ) .await; assert!(resp.is_err()); assert_grpc_status(resp.err(), Code::NotFound); // Registered service - initial state let resp = service - .check(Request::new(HealthCheckRequest { - service: "TestService".to_string(), - })) + .check( + &arena, + Request::new(HealthCheckRequest { + service: "TestService", + }), + ) .await; assert!(resp.is_ok()); let resp = resp.unwrap().into_inner(); assert_serving_status(resp.status, ServingStatus::Unknown); // Registered service - updated state - reporter - .set_service_status("TestService", ServingStatus::Serving) - .await; + reporter.set_service_status("TestService", ServingStatus::Serving); let resp = service - .check(Request::new(HealthCheckRequest { - service: "TestService".to_string(), - })) + .check( + &arena, + Request::new(HealthCheckRequest { + service: "TestService", + }), + ) .await; assert!(resp.is_ok()); let resp = resp.unwrap().into_inner(); assert_serving_status(resp.status, ServingStatus::Serving); } - #[tokio::test] + #[monoio::test_all] async fn test_service_watch() { - let (mut reporter, service) = make_test_service().await; + let arena = defiant::Arena::new(); + let (mut reporter, service) = make_test_service(); // Overall server health let resp = service - .watch(Request::new(HealthCheckRequest { - service: "".to_string(), - })) + .watch( + &arena, + Request::new(HealthCheckRequest { service: "" }), + ) .await; assert!(resp.is_ok()); let mut resp = resp.unwrap().into_inner(); @@ -298,23 +358,29 @@ mod tests { // Unregistered service let resp = service - .watch(Request::new(HealthCheckRequest { - service: "Unregistered".to_string(), - })) + .watch( + &arena, + Request::new(HealthCheckRequest { + service: "Unregistered", + }), + ) .await; assert!(resp.is_err()); assert_grpc_status(resp.err(), Code::NotFound); // Registered service let resp = service - .watch(Request::new(HealthCheckRequest { - service: "TestService".to_string(), - })) + .watch( + &arena, + Request::new(HealthCheckRequest { + service: "TestService", + }), + ) .await; assert!(resp.is_ok()); let mut resp = resp.unwrap().into_inner(); - // Registered service - initial state + // Registered service - initial state (first poll yields current value) let item = resp .next() .await @@ -323,9 +389,7 @@ mod tests { assert_serving_status(item.status, ServingStatus::Unknown); // Registered service - updated state - reporter - .set_service_status("TestService", ServingStatus::NotServing) - .await; + reporter.set_service_status("TestService", ServingStatus::NotServing); let item = resp .next() @@ -335,9 +399,7 @@ mod tests { assert_serving_status(item.status, ServingStatus::NotServing); // Registered service - updated state - reporter - .set_service_status("TestService", ServingStatus::Serving) - .await; + reporter.set_service_status("TestService", ServingStatus::Serving); let item = resp .next() .await @@ -346,7 +408,7 @@ mod tests { assert_serving_status(item.status, ServingStatus::Serving); // De-registered service - reporter.clear_service_status("TestService").await; + reporter.clear_service_status("TestService"); let item = resp.next().await; assert!(item.is_none()); } diff --git a/tocin-reflection/Cargo.toml b/tocin-reflection/Cargo.toml index d8abcf2..8f358c0 100644 --- a/tocin-reflection/Cargo.toml +++ b/tocin-reflection/Cargo.toml @@ -21,16 +21,16 @@ rust-version = { workspace = true } all-features = true [features] -server = ["dep:prost-types", "dep:tokio", "dep:tokio-stream"] +server = ["dep:prost-types"] default = ["server"] [dependencies] +defiant = { path = "../../defiant/defiant" } +futures-core = "0.3" +futures-util = "0.3" prost = "0.14" prost-types = {version = "0.14", optional = true} -tokio = { version = "1.0", features = ["sync", "rt"], optional = true } -tokio-stream = {version = "0.1", default-features = false, optional = true } tocin = { version = "0.14.0", path = "../tocin", default-features = false, features = ["codegen"] } -tocin-defiant = { version = "0.14.0", path = "../tocin-defiant", default-features = false } [dev-dependencies] monoio = { workspace = true, features = ["macros"] } @@ -53,5 +53,4 @@ allowed_external_types = [ "prost_types::*", "futures_core::stream::Stream", - "tower_service::Service", ] diff --git a/tocin-reflection/src/generated/grpc_reflection_v1.rs b/tocin-reflection/src/generated/grpc_reflection_v1.rs index c281f39..7a15112 100644 --- a/tocin-reflection/src/generated/grpc_reflection_v1.rs +++ b/tocin-reflection/src/generated/grpc_reflection_v1.rs @@ -144,121 +144,6 @@ pub struct ErrorResponse { #[prost(string, tag = "2")] pub error_message: ::prost::alloc::string::String, } -/// Generated client implementations. -pub mod server_reflection_client { - #![allow( - unused_variables, - dead_code, - missing_docs, - clippy::wildcard_imports, - clippy::let_unit_value, - )] - use tocin::codegen::*; - use tocin::codegen::http::Uri; - #[derive(Debug, Clone)] - pub struct ServerReflectionClient { - inner: tocin::client::Grpc, - } - impl ServerReflectionClient - where - T: tocin::client::GrpcService, - T::Error: Into, - T::ResponseBody: Body + std::marker::Send + 'static, - ::Error: Into + std::marker::Send, - { - pub fn new(inner: T) -> Self { - let inner = tocin::client::Grpc::new(inner); - Self { inner } - } - pub fn with_origin(inner: T, origin: Uri) -> Self { - let inner = tocin::client::Grpc::with_origin(inner, origin); - Self { inner } - } - pub fn with_interceptor( - inner: T, - interceptor: F, - ) -> ServerReflectionClient> - where - F: tocin::service::Interceptor, - T::ResponseBody: Default, - T: tocin::codegen::Service< - http::Request, - Response = http::Response< - >::ResponseBody, - >, - >, - , - >>::Error: Into + std::marker::Send + std::marker::Sync, - { - ServerReflectionClient::new(InterceptedService::new(inner, interceptor)) - } - /// Compress requests with the given encoding. - /// - /// This requires the server to support it otherwise it might respond with an - /// error. - #[must_use] - pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self { - self.inner = self.inner.send_compressed(encoding); - self - } - /// Enable decompressing responses. - #[must_use] - pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self { - self.inner = self.inner.accept_compressed(encoding); - self - } - /// Limits the maximum size of a decoded message. - /// - /// Default: `4MB` - #[must_use] - pub fn max_decoding_message_size(mut self, limit: usize) -> Self { - self.inner = self.inner.max_decoding_message_size(limit); - self - } - /// Limits the maximum size of an encoded message. - /// - /// Default: `usize::MAX` - #[must_use] - pub fn max_encoding_message_size(mut self, limit: usize) -> Self { - self.inner = self.inner.max_encoding_message_size(limit); - self - } - /// The reflection service is structured as a bidirectional stream, ensuring - /// all related requests go to a single server. - pub async fn server_reflection_info( - &mut self, - request: impl tocin::IntoStreamingRequest< - Message = super::ServerReflectionRequest, - >, - ) -> std::result::Result< - tocin::Response>, - tocin::Status, - > { - self.inner - .ready() - .await - .map_err(|e| { - tocin::Status::unknown( - format!("Service was not ready: {}", e.into()), - ) - })?; - let codec = tocin_defiant::ProstCodec::default(); - let path = http::uri::PathAndQuery::from_static( - "/grpc.reflection.v1.ServerReflection/ServerReflectionInfo", - ); - let mut req = request.into_streaming_request(); - req.extensions_mut() - .insert( - GrpcMethod::new( - "grpc.reflection.v1.ServerReflection", - "ServerReflectionInfo", - ), - ); - self.inner.streaming(req, path, codec).await - } - } -} /// Generated server implementations. pub mod server_reflection_server { #![allow( @@ -270,22 +155,22 @@ pub mod server_reflection_server { )] use tocin::codegen::*; /// Generated trait containing gRPC methods that should be implemented for use with ServerReflectionServer. - #[async_trait] - pub trait ServerReflection: std::marker::Send + std::marker::Sync + 'static { + #[async_trait(?Send)] + pub trait ServerReflection: 'static { /// Server streaming response type for the ServerReflectionInfo method. - type ServerReflectionInfoStream: tocin::codegen::tokio_stream::Stream< + type ServerReflectionInfoStream: tocin::codegen::Stream< Item = std::result::Result< super::ServerReflectionResponse, tocin::Status, >, > - + std::marker::Send + 'static; /// The reflection service is structured as a bidirectional stream, ensuring /// all related requests go to a single server. - async fn server_reflection_info( + async fn server_reflection_info<'arena>( &self, - request: tocin::Request>, + arena: &'arena defiant::Arena, + request: tocin::Request>>, ) -> std::result::Result< tocin::Response, tocin::Status, @@ -312,15 +197,6 @@ pub mod server_reflection_server { max_encoding_message_size: None, } } - pub fn with_interceptor( - inner: T, - interceptor: F, - ) -> InterceptedService - where - F: tocin::service::Interceptor, - { - InterceptedService::new(Self::new(inner), interceptor) - } /// Enable decompressing requests with the given encoding. #[must_use] pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self { @@ -350,62 +226,68 @@ pub mod server_reflection_server { self } } - impl tocin::codegen::Service> for ServerReflectionServer + impl GrpcService for ServerReflectionServer where T: ServerReflection, - B: Body + std::marker::Send + 'static, - B::Error: Into + std::marker::Send + 'static, { - type Response = http::Response; - type Error = std::convert::Infallible; - type Future = BoxFuture; - fn poll_ready( - &mut self, - _cx: &mut Context<'_>, - ) -> Poll> { - Poll::Ready(Ok(())) - } - fn call(&mut self, req: http::Request) -> Self::Future { - match req.uri().path() { - "/grpc.reflection.v1.ServerReflection/ServerReflectionInfo" => { - #[allow(non_camel_case_types)] - struct ServerReflectionInfoSvc(pub Arc); - impl< - T: ServerReflection, - > tocin::server::StreamingService - for ServerReflectionInfoSvc { - type Response = super::ServerReflectionResponse; - type ResponseStream = T::ServerReflectionInfoStream; - type Future = BoxFuture< - tocin::Response, - tocin::Status, - >; - fn call( - &mut self, - request: tocin::Request< - tocin::Streaming, - >, - ) -> Self::Future { - let inner = Arc::clone(&self.0); - let fut = async move { - ::server_reflection_info( - &inner, - request, - ) - .await - }; - Box::pin(fut) + type CallFuture<'arena> = std::pin::Pin< + Box> + 'arena>, + > + where + Self: 'arena; + fn call<'arena>( + &'arena mut self, + arena: &'arena defiant::Arena, + req: http::Request, + mut respond: h2::server::SendResponse, + ) -> Self::CallFuture<'arena> { + Box::pin(async move { + match req.uri().path() { + "/grpc.reflection.v1.ServerReflection/ServerReflectionInfo" => { + #[allow(non_camel_case_types)] + struct ServerReflectionInfoSvc(pub Arc); + impl< + 'arena, + T: ServerReflection, + > tocin::server::StreamingService< + 'arena, + crate::prost_codec::ProstStdDecoder, + > for ServerReflectionInfoSvc { + type Response = super::ServerReflectionResponse; + type ResponseStream = T::ServerReflectionInfoStream; + type Future = BoxFuture< + 'arena, + tocin::Response, + tocin::Status, + >; + fn call( + &mut self, + arena: &'arena defiant::Arena, + request: tocin::Request< + tocin::Streaming<'arena, crate::prost_codec::ProstStdDecoder>, + >, + ) -> Self::Future { + let inner = Arc::clone(&self.0); + let fut = async move { + ::server_reflection_info( + &inner, + arena, + request, + ) + .await + }; + Box::pin(fut) + } } - } - let accept_compression_encodings = self.accept_compression_encodings; - let send_compression_encodings = self.send_compression_encodings; - let max_decoding_message_size = self.max_decoding_message_size; - let max_encoding_message_size = self.max_encoding_message_size; - let inner = self.inner.clone(); - let fut = async move { + let accept_compression_encodings = self + .accept_compression_encodings; + let send_compression_encodings = self.send_compression_encodings; + let max_decoding_message_size = self.max_decoding_message_size; + let max_encoding_message_size = self.max_encoding_message_size; + let inner = self.inner.clone(); let method = ServerReflectionInfoSvc(inner); - let codec = tocin_defiant::ProstCodec::default(); - let mut grpc = tocin::server::Grpc::new(codec) + let codec = crate::prost_codec::ProstStdCodec::::default(); + let mut grpc = tocin::server::Grpc::new(codec, arena) .apply_compression_config( accept_compression_encodings, send_compression_encodings, @@ -414,31 +296,38 @@ pub mod server_reflection_server { max_decoding_message_size, max_encoding_message_size, ); - let res = grpc.streaming(method, req).await; - Ok(res) - }; - Box::pin(fut) - } - _ => { - Box::pin(async move { - let mut response = http::Response::new( - tocin::body::Body::default(), + grpc.streaming(method, req, respond).await?; + Ok(()) + } + _ => { + let status = tocin::Status::unimplemented( + format!("Unknown method {}", req.uri().path()), ); - let headers = response.headers_mut(); - headers - .insert( - tocin::Status::GRPC_STATUS, - (tocin::Code::Unimplemented as i32).into(), - ); - headers - .insert( + let response = http::Response::builder() + .status(http::StatusCode::OK) + .header( http::header::CONTENT_TYPE, tocin::metadata::GRPC_CONTENT_TYPE, - ); - Ok(response) - }) + ) + .body(()) + .map_err(|e| tocin::Status::internal( + format!("Failed to build error response: {}", e), + ))?; + let mut send_stream = respond + .send_response(response, false) + .map_err(|e| tocin::Status::internal( + format!("h2 error: {}", e), + ))?; + let trailers = status.to_header_map()?; + send_stream + .send_trailers(trailers) + .map_err(|e| tocin::Status::internal( + format!("h2 error: {}", e), + ))?; + Ok(()) + } } - } + }) } } impl Clone for ServerReflectionServer { diff --git a/tocin-reflection/src/generated/grpc_reflection_v1alpha.rs b/tocin-reflection/src/generated/grpc_reflection_v1alpha.rs index 077a357..2ad1a1c 100644 --- a/tocin-reflection/src/generated/grpc_reflection_v1alpha.rs +++ b/tocin-reflection/src/generated/grpc_reflection_v1alpha.rs @@ -144,121 +144,6 @@ pub struct ErrorResponse { #[prost(string, tag = "2")] pub error_message: ::prost::alloc::string::String, } -/// Generated client implementations. -pub mod server_reflection_client { - #![allow( - unused_variables, - dead_code, - missing_docs, - clippy::wildcard_imports, - clippy::let_unit_value, - )] - use tocin::codegen::*; - use tocin::codegen::http::Uri; - #[derive(Debug, Clone)] - pub struct ServerReflectionClient { - inner: tocin::client::Grpc, - } - impl ServerReflectionClient - where - T: tocin::client::GrpcService, - T::Error: Into, - T::ResponseBody: Body + std::marker::Send + 'static, - ::Error: Into + std::marker::Send, - { - pub fn new(inner: T) -> Self { - let inner = tocin::client::Grpc::new(inner); - Self { inner } - } - pub fn with_origin(inner: T, origin: Uri) -> Self { - let inner = tocin::client::Grpc::with_origin(inner, origin); - Self { inner } - } - pub fn with_interceptor( - inner: T, - interceptor: F, - ) -> ServerReflectionClient> - where - F: tocin::service::Interceptor, - T::ResponseBody: Default, - T: tocin::codegen::Service< - http::Request, - Response = http::Response< - >::ResponseBody, - >, - >, - , - >>::Error: Into + std::marker::Send + std::marker::Sync, - { - ServerReflectionClient::new(InterceptedService::new(inner, interceptor)) - } - /// Compress requests with the given encoding. - /// - /// This requires the server to support it otherwise it might respond with an - /// error. - #[must_use] - pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self { - self.inner = self.inner.send_compressed(encoding); - self - } - /// Enable decompressing responses. - #[must_use] - pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self { - self.inner = self.inner.accept_compressed(encoding); - self - } - /// Limits the maximum size of a decoded message. - /// - /// Default: `4MB` - #[must_use] - pub fn max_decoding_message_size(mut self, limit: usize) -> Self { - self.inner = self.inner.max_decoding_message_size(limit); - self - } - /// Limits the maximum size of an encoded message. - /// - /// Default: `usize::MAX` - #[must_use] - pub fn max_encoding_message_size(mut self, limit: usize) -> Self { - self.inner = self.inner.max_encoding_message_size(limit); - self - } - /// The reflection service is structured as a bidirectional stream, ensuring - /// all related requests go to a single server. - pub async fn server_reflection_info( - &mut self, - request: impl tocin::IntoStreamingRequest< - Message = super::ServerReflectionRequest, - >, - ) -> std::result::Result< - tocin::Response>, - tocin::Status, - > { - self.inner - .ready() - .await - .map_err(|e| { - tocin::Status::unknown( - format!("Service was not ready: {}", e.into()), - ) - })?; - let codec = tocin_defiant::ProstCodec::default(); - let path = http::uri::PathAndQuery::from_static( - "/grpc.reflection.v1alpha.ServerReflection/ServerReflectionInfo", - ); - let mut req = request.into_streaming_request(); - req.extensions_mut() - .insert( - GrpcMethod::new( - "grpc.reflection.v1alpha.ServerReflection", - "ServerReflectionInfo", - ), - ); - self.inner.streaming(req, path, codec).await - } - } -} /// Generated server implementations. pub mod server_reflection_server { #![allow( @@ -270,22 +155,22 @@ pub mod server_reflection_server { )] use tocin::codegen::*; /// Generated trait containing gRPC methods that should be implemented for use with ServerReflectionServer. - #[async_trait] - pub trait ServerReflection: std::marker::Send + std::marker::Sync + 'static { + #[async_trait(?Send)] + pub trait ServerReflection: 'static { /// Server streaming response type for the ServerReflectionInfo method. - type ServerReflectionInfoStream: tocin::codegen::tokio_stream::Stream< + type ServerReflectionInfoStream: tocin::codegen::Stream< Item = std::result::Result< super::ServerReflectionResponse, tocin::Status, >, > - + std::marker::Send + 'static; /// The reflection service is structured as a bidirectional stream, ensuring /// all related requests go to a single server. - async fn server_reflection_info( + async fn server_reflection_info<'arena>( &self, - request: tocin::Request>, + arena: &'arena defiant::Arena, + request: tocin::Request>>, ) -> std::result::Result< tocin::Response, tocin::Status, @@ -312,15 +197,6 @@ pub mod server_reflection_server { max_encoding_message_size: None, } } - pub fn with_interceptor( - inner: T, - interceptor: F, - ) -> InterceptedService - where - F: tocin::service::Interceptor, - { - InterceptedService::new(Self::new(inner), interceptor) - } /// Enable decompressing requests with the given encoding. #[must_use] pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self { @@ -350,62 +226,68 @@ pub mod server_reflection_server { self } } - impl tocin::codegen::Service> for ServerReflectionServer + impl GrpcService for ServerReflectionServer where T: ServerReflection, - B: Body + std::marker::Send + 'static, - B::Error: Into + std::marker::Send + 'static, { - type Response = http::Response; - type Error = std::convert::Infallible; - type Future = BoxFuture; - fn poll_ready( - &mut self, - _cx: &mut Context<'_>, - ) -> Poll> { - Poll::Ready(Ok(())) - } - fn call(&mut self, req: http::Request) -> Self::Future { - match req.uri().path() { - "/grpc.reflection.v1alpha.ServerReflection/ServerReflectionInfo" => { - #[allow(non_camel_case_types)] - struct ServerReflectionInfoSvc(pub Arc); - impl< - T: ServerReflection, - > tocin::server::StreamingService - for ServerReflectionInfoSvc { - type Response = super::ServerReflectionResponse; - type ResponseStream = T::ServerReflectionInfoStream; - type Future = BoxFuture< - tocin::Response, - tocin::Status, - >; - fn call( - &mut self, - request: tocin::Request< - tocin::Streaming, - >, - ) -> Self::Future { - let inner = Arc::clone(&self.0); - let fut = async move { - ::server_reflection_info( - &inner, - request, - ) - .await - }; - Box::pin(fut) + type CallFuture<'arena> = std::pin::Pin< + Box> + 'arena>, + > + where + Self: 'arena; + fn call<'arena>( + &'arena mut self, + arena: &'arena defiant::Arena, + req: http::Request, + mut respond: h2::server::SendResponse, + ) -> Self::CallFuture<'arena> { + Box::pin(async move { + match req.uri().path() { + "/grpc.reflection.v1alpha.ServerReflection/ServerReflectionInfo" => { + #[allow(non_camel_case_types)] + struct ServerReflectionInfoSvc(pub Arc); + impl< + 'arena, + T: ServerReflection, + > tocin::server::StreamingService< + 'arena, + crate::prost_codec::ProstStdDecoder, + > for ServerReflectionInfoSvc { + type Response = super::ServerReflectionResponse; + type ResponseStream = T::ServerReflectionInfoStream; + type Future = BoxFuture< + 'arena, + tocin::Response, + tocin::Status, + >; + fn call( + &mut self, + arena: &'arena defiant::Arena, + request: tocin::Request< + tocin::Streaming<'arena, crate::prost_codec::ProstStdDecoder>, + >, + ) -> Self::Future { + let inner = Arc::clone(&self.0); + let fut = async move { + ::server_reflection_info( + &inner, + arena, + request, + ) + .await + }; + Box::pin(fut) + } } - } - let accept_compression_encodings = self.accept_compression_encodings; - let send_compression_encodings = self.send_compression_encodings; - let max_decoding_message_size = self.max_decoding_message_size; - let max_encoding_message_size = self.max_encoding_message_size; - let inner = self.inner.clone(); - let fut = async move { + let accept_compression_encodings = self + .accept_compression_encodings; + let send_compression_encodings = self.send_compression_encodings; + let max_decoding_message_size = self.max_decoding_message_size; + let max_encoding_message_size = self.max_encoding_message_size; + let inner = self.inner.clone(); let method = ServerReflectionInfoSvc(inner); - let codec = tocin_defiant::ProstCodec::default(); - let mut grpc = tocin::server::Grpc::new(codec) + let codec = crate::prost_codec::ProstStdCodec::::default(); + let mut grpc = tocin::server::Grpc::new(codec, arena) .apply_compression_config( accept_compression_encodings, send_compression_encodings, @@ -414,31 +296,38 @@ pub mod server_reflection_server { max_decoding_message_size, max_encoding_message_size, ); - let res = grpc.streaming(method, req).await; - Ok(res) - }; - Box::pin(fut) - } - _ => { - Box::pin(async move { - let mut response = http::Response::new( - tocin::body::Body::default(), + grpc.streaming(method, req, respond).await?; + Ok(()) + } + _ => { + let status = tocin::Status::unimplemented( + format!("Unknown method {}", req.uri().path()), ); - let headers = response.headers_mut(); - headers - .insert( - tocin::Status::GRPC_STATUS, - (tocin::Code::Unimplemented as i32).into(), - ); - headers - .insert( + let response = http::Response::builder() + .status(http::StatusCode::OK) + .header( http::header::CONTENT_TYPE, tocin::metadata::GRPC_CONTENT_TYPE, - ); - Ok(response) - }) + ) + .body(()) + .map_err(|e| tocin::Status::internal( + format!("Failed to build error response: {}", e), + ))?; + let mut send_stream = respond + .send_response(response, false) + .map_err(|e| tocin::Status::internal( + format!("h2 error: {}", e), + ))?; + let trailers = status.to_header_map()?; + send_stream + .send_trailers(trailers) + .map_err(|e| tocin::Status::internal( + format!("h2 error: {}", e), + ))?; + Ok(()) + } } - } + }) } } impl Clone for ServerReflectionServer { diff --git a/tocin-reflection/src/lib.rs b/tocin-reflection/src/lib.rs index 97ea316..f3d1087 100644 --- a/tocin-reflection/src/lib.rs +++ b/tocin-reflection/src/lib.rs @@ -7,6 +7,9 @@ #![doc(test(no_crate_inject, attr(deny(rust_2018_idioms))))] #![cfg_attr(docsrs, feature(doc_auto_cfg))] +#[doc(hidden)] +pub mod prost_codec; + mod generated { #![allow(unreachable_pub)] #![allow(missing_docs)] diff --git a/tocin-reflection/src/prost_codec.rs b/tocin-reflection/src/prost_codec.rs new file mode 100644 index 0000000..ac614fe --- /dev/null +++ b/tocin-reflection/src/prost_codec.rs @@ -0,0 +1,90 @@ +//! A codec bridging standard `prost::Message` types to the tocin arena-based codec interface. +//! +//! The gRPC reflection service uses standard `prost::Message` types (not arena-allocated +//! defiant views), so we need a codec that decodes prost messages but satisfies the +//! `tocin::codec::Codec<'arena>` trait. + +use std::marker::PhantomData; +use tocin::codec::{BufferSettings, Codec, DecodeBuf, Decoder, EncodeBuf, Encoder}; +use tocin::Status; + +/// A codec for standard `prost::Message` types that satisfies the tocin arena-based +/// codec interface. The arena parameter is accepted but unused since prost messages +/// are fully owned (no arena borrowing). +#[derive(Debug, Clone)] +pub(crate) struct ProstStdCodec { + _pd: PhantomData<(T, U)>, +} + +impl Default for ProstStdCodec { + fn default() -> Self { + Self { _pd: PhantomData } + } +} + +impl<'arena, T, U> Codec<'arena> for ProstStdCodec +where + T: prost::Message + Default + 'static, + U: prost::Message + Default + 'static, +{ + type Encode = T; + type Decode = U; + + type Encoder = ProstStdEncoder; + type Decoder = ProstStdDecoder; + + fn encoder(&mut self) -> Self::Encoder { + ProstStdEncoder { _pd: PhantomData } + } + + fn decoder(&mut self) -> Self::Decoder { + ProstStdDecoder { _pd: PhantomData } + } +} + +/// Encoder for standard prost messages. +#[derive(Debug, Clone, Default)] +pub(crate) struct ProstStdEncoder { + _pd: PhantomData, +} + +impl Encoder for ProstStdEncoder { + type Item = T; + type Error = Status; + + fn encode(&mut self, item: Self::Item, buf: &mut EncodeBuf<'_>) -> Result<(), Self::Error> { + item.encode(buf) + .expect("Message only errors if not enough space"); + Ok(()) + } + + fn buffer_settings(&self) -> BufferSettings { + BufferSettings::default() + } +} + +/// Decoder for standard prost messages. Ignores the arena since prost messages are owned. +#[derive(Debug, Clone, Default)] +pub struct ProstStdDecoder { + _pd: PhantomData, +} + +impl<'arena, U: prost::Message + Default + 'static> Decoder<'arena> for ProstStdDecoder { + type Item = U; + type Error = Status; + + fn decode( + &mut self, + buf: &mut DecodeBuf<'_>, + _arena: &'arena defiant::Arena, + ) -> Result, Self::Error> { + let item = U::decode(buf) + .map(Some) + .map_err(|e| Status::internal(e.to_string()))?; + Ok(item) + } + + fn buffer_settings(&self) -> BufferSettings { + BufferSettings::default() + } +} diff --git a/tocin-reflection/src/server/v1.rs b/tocin-reflection/src/server/v1.rs index f29c7c0..b594922 100644 --- a/tocin-reflection/src/server/v1.rs +++ b/tocin-reflection/src/server/v1.rs @@ -1,8 +1,7 @@ -use std::{fmt, sync::Arc}; +use std::sync::Arc; -use tokio::sync::mpsc; -use tokio_stream::{Stream, StreamExt}; -use tocin::{Request, Response, Status, Streaming}; +use futures_core::Stream; +use tocin::{Request, Response, Status}; use super::ReflectionServiceState; use crate::pb::v1::server_reflection_request::MessageRequest; @@ -12,6 +11,7 @@ use crate::pb::v1::{ ExtensionNumberResponse, FileDescriptorResponse, ListServiceResponse, ServerReflectionRequest, ServerReflectionResponse, ServiceResponse, }; +use crate::prost_codec::ProstStdDecoder; /// An implementation for `ServerReflection`. #[derive(Debug)] @@ -19,35 +19,84 @@ pub struct ReflectionService { state: Arc, } -#[tocin::async_trait] +#[tocin::async_trait(?Send)] impl ServerReflection for ReflectionService { type ServerReflectionInfoStream = ServerReflectionInfoStream; - async fn server_reflection_info( + async fn server_reflection_info<'arena>( &self, - req: Request>, + _arena: &'arena defiant::Arena, + req: Request>>, ) -> Result, Status> { + // Drain the incoming request stream into a Vec, since we need to return a + // 'static response stream but the request Streaming borrows from 'arena. let mut req_rx = req.into_inner(); - let (resp_tx, resp_rx) = mpsc::channel::>(1); + let mut requests = Vec::new(); + while let Some(item) = futures_util::StreamExt::next(&mut req_rx).await { + match item { + Ok(req) => requests.push(req), + Err(_) => break, + } + } - let state = self.state.clone(); + Ok(Response::new(ServerReflectionInfoStream::new( + self.state.clone(), + requests, + ))) + } +} - tokio::spawn(async move { - while let Some(req) = req_rx.next().await { - let Ok(req) = req else { - return; - }; +impl From for ReflectionService { + fn from(state: ReflectionServiceState) -> Self { + Self { + state: Arc::new(state), + } + } +} + +/// A response stream that processes buffered requests and yields responses. +pub struct ServerReflectionInfoStream { + state: Arc, + requests: std::vec::IntoIter, + done: bool, +} + +impl ServerReflectionInfoStream { + fn new(state: Arc, requests: Vec) -> Self { + Self { + state, + requests: requests.into_iter(), + done: false, + } + } +} + +impl Stream for ServerReflectionInfoStream { + type Item = Result; + + fn poll_next( + mut self: std::pin::Pin<&mut Self>, + _cx: &mut std::task::Context<'_>, + ) -> std::task::Poll> { + if self.done { + return std::task::Poll::Ready(None); + } + match self.requests.next() { + None => std::task::Poll::Ready(None), + Some(req) => { let resp_msg = match req.message_request.clone() { None => Err(Status::invalid_argument("invalid MessageRequest")), Some(msg) => match msg { - MessageRequest::FileByFilename(s) => state.file_by_filename(&s).map(|fd| { - MessageResponse::FileDescriptorResponse(FileDescriptorResponse { - file_descriptor_proto: vec![fd], + MessageRequest::FileByFilename(s) => { + self.state.file_by_filename(&s).map(|fd| { + MessageResponse::FileDescriptorResponse(FileDescriptorResponse { + file_descriptor_proto: vec![fd], + }) }) - }), + } MessageRequest::FileContainingSymbol(s) => { - state.symbol_by_name(&s).map(|fd| { + self.state.symbol_by_name(&s).map(|fd| { MessageResponse::FileDescriptorResponse(FileDescriptorResponse { file_descriptor_proto: vec![fd], }) @@ -57,15 +106,14 @@ impl ServerReflection for ReflectionService { Err(Status::not_found("extensions are not supported")) } MessageRequest::AllExtensionNumbersOfType(_) => { - // NOTE: Workaround. Some grpc clients (e.g. grpcurl) expect this method not to fail. - // https://github.com/hyperium/tonic/issues/1077 Ok(MessageResponse::AllExtensionNumbersResponse( ExtensionNumberResponse::default(), )) } MessageRequest::ListServices(_) => { Ok(MessageResponse::ListServicesResponse(ListServiceResponse { - service: state + service: self + .state .list_services() .iter() .map(|s| ServiceResponse { name: s.clone() }) @@ -79,60 +127,27 @@ impl ServerReflection for ReflectionService { Ok(resp_msg) => { let resp = ServerReflectionResponse { valid_host: req.host.clone(), - original_request: Some(req.clone()), + original_request: Some(req), message_response: Some(resp_msg), }; - resp_tx.send(Ok(resp)).await.expect("send"); + std::task::Poll::Ready(Some(Ok(resp))) } Err(status) => { - resp_tx.send(Err(status)).await.expect("send"); - return; + self.done = true; + std::task::Poll::Ready(Some(Err(status))) } } } - }); - - Ok(Response::new(ServerReflectionInfoStream::new(resp_rx))) - } -} - -impl From for ReflectionService { - fn from(state: ReflectionServiceState) -> Self { - Self { - state: Arc::new(state), } } -} - -/// A response stream. -pub struct ServerReflectionInfoStream { - inner: tokio_stream::wrappers::ReceiverStream>, -} - -impl ServerReflectionInfoStream { - fn new(resp_rx: mpsc::Receiver>) -> Self { - let inner = tokio_stream::wrappers::ReceiverStream::new(resp_rx); - Self { inner } - } -} - -impl Stream for ServerReflectionInfoStream { - type Item = Result; - - fn poll_next( - mut self: std::pin::Pin<&mut Self>, - cx: &mut std::task::Context<'_>, - ) -> std::task::Poll> { - std::pin::Pin::new(&mut self.inner).poll_next(cx) - } fn size_hint(&self) -> (usize, Option) { - self.inner.size_hint() + self.requests.size_hint() } } -impl fmt::Debug for ServerReflectionInfoStream { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { +impl std::fmt::Debug for ServerReflectionInfoStream { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_tuple("ServerReflectionInfoStream").finish() } } diff --git a/tocin-reflection/src/server/v1alpha.rs b/tocin-reflection/src/server/v1alpha.rs index 06fbd7c..c88d527 100644 --- a/tocin-reflection/src/server/v1alpha.rs +++ b/tocin-reflection/src/server/v1alpha.rs @@ -1,8 +1,7 @@ -use std::{fmt, sync::Arc}; +use std::sync::Arc; -use tokio::sync::mpsc; -use tokio_stream::{Stream, StreamExt}; -use tocin::{Request, Response, Status, Streaming}; +use futures_core::Stream; +use tocin::{Request, Response, Status}; use super::ReflectionServiceState; use crate::pb::v1alpha::server_reflection_request::MessageRequest; @@ -12,6 +11,7 @@ use crate::pb::v1alpha::{ ExtensionNumberResponse, FileDescriptorResponse, ListServiceResponse, ServerReflectionRequest, ServerReflectionResponse, ServiceResponse, }; +use crate::prost_codec::ProstStdDecoder; /// An implementation for `ServerReflection`. #[derive(Debug)] @@ -19,35 +19,84 @@ pub struct ReflectionService { state: Arc, } -#[tocin::async_trait] +#[tocin::async_trait(?Send)] impl ServerReflection for ReflectionService { type ServerReflectionInfoStream = ServerReflectionInfoStream; - async fn server_reflection_info( + async fn server_reflection_info<'arena>( &self, - req: Request>, + _arena: &'arena defiant::Arena, + req: Request>>, ) -> Result, Status> { + // Drain the incoming request stream into a Vec, since we need to return a + // 'static response stream but the request Streaming borrows from 'arena. let mut req_rx = req.into_inner(); - let (resp_tx, resp_rx) = mpsc::channel::>(1); + let mut requests = Vec::new(); + while let Some(item) = futures_util::StreamExt::next(&mut req_rx).await { + match item { + Ok(req) => requests.push(req), + Err(_) => break, + } + } - let state = self.state.clone(); + Ok(Response::new(ServerReflectionInfoStream::new( + self.state.clone(), + requests, + ))) + } +} - tokio::spawn(async move { - while let Some(req) = req_rx.next().await { - let Ok(req) = req else { - return; - }; +impl From for ReflectionService { + fn from(state: ReflectionServiceState) -> Self { + Self { + state: Arc::new(state), + } + } +} + +/// A response stream that processes buffered requests and yields responses. +pub struct ServerReflectionInfoStream { + state: Arc, + requests: std::vec::IntoIter, + done: bool, +} + +impl ServerReflectionInfoStream { + fn new(state: Arc, requests: Vec) -> Self { + Self { + state, + requests: requests.into_iter(), + done: false, + } + } +} + +impl Stream for ServerReflectionInfoStream { + type Item = Result; + + fn poll_next( + mut self: std::pin::Pin<&mut Self>, + _cx: &mut std::task::Context<'_>, + ) -> std::task::Poll> { + if self.done { + return std::task::Poll::Ready(None); + } + match self.requests.next() { + None => std::task::Poll::Ready(None), + Some(req) => { let resp_msg = match req.message_request.clone() { None => Err(Status::invalid_argument("invalid MessageRequest")), Some(msg) => match msg { - MessageRequest::FileByFilename(s) => state.file_by_filename(&s).map(|fd| { - MessageResponse::FileDescriptorResponse(FileDescriptorResponse { - file_descriptor_proto: vec![fd], + MessageRequest::FileByFilename(s) => { + self.state.file_by_filename(&s).map(|fd| { + MessageResponse::FileDescriptorResponse(FileDescriptorResponse { + file_descriptor_proto: vec![fd], + }) }) - }), + } MessageRequest::FileContainingSymbol(s) => { - state.symbol_by_name(&s).map(|fd| { + self.state.symbol_by_name(&s).map(|fd| { MessageResponse::FileDescriptorResponse(FileDescriptorResponse { file_descriptor_proto: vec![fd], }) @@ -57,15 +106,14 @@ impl ServerReflection for ReflectionService { Err(Status::not_found("extensions are not supported")) } MessageRequest::AllExtensionNumbersOfType(_) => { - // NOTE: Workaround. Some grpc clients (e.g. grpcurl) expect this method not to fail. - // https://github.com/hyperium/tonic/issues/1077 Ok(MessageResponse::AllExtensionNumbersResponse( ExtensionNumberResponse::default(), )) } MessageRequest::ListServices(_) => { Ok(MessageResponse::ListServicesResponse(ListServiceResponse { - service: state + service: self + .state .list_services() .iter() .map(|s| ServiceResponse { name: s.clone() }) @@ -79,60 +127,27 @@ impl ServerReflection for ReflectionService { Ok(resp_msg) => { let resp = ServerReflectionResponse { valid_host: req.host.clone(), - original_request: Some(req.clone()), + original_request: Some(req), message_response: Some(resp_msg), }; - resp_tx.send(Ok(resp)).await.expect("send"); + std::task::Poll::Ready(Some(Ok(resp))) } Err(status) => { - resp_tx.send(Err(status)).await.expect("send"); - return; + self.done = true; + std::task::Poll::Ready(Some(Err(status))) } } } - }); - - Ok(Response::new(ServerReflectionInfoStream::new(resp_rx))) - } -} - -impl From for ReflectionService { - fn from(state: ReflectionServiceState) -> Self { - Self { - state: Arc::new(state), } } -} - -/// A response stream. -pub struct ServerReflectionInfoStream { - inner: tokio_stream::wrappers::ReceiverStream>, -} - -impl ServerReflectionInfoStream { - fn new(resp_rx: mpsc::Receiver>) -> Self { - let inner = tokio_stream::wrappers::ReceiverStream::new(resp_rx); - Self { inner } - } -} - -impl Stream for ServerReflectionInfoStream { - type Item = Result; - - fn poll_next( - mut self: std::pin::Pin<&mut Self>, - cx: &mut std::task::Context<'_>, - ) -> std::task::Poll> { - std::pin::Pin::new(&mut self.inner).poll_next(cx) - } fn size_hint(&self) -> (usize, Option) { - self.inner.size_hint() + self.requests.size_hint() } } -impl fmt::Debug for ServerReflectionInfoStream { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { +impl std::fmt::Debug for ServerReflectionInfoStream { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_tuple("ServerReflectionInfoStream").finish() } } diff --git a/tocin-web/Cargo.toml b/tocin-web/Cargo.toml deleted file mode 100644 index 02cdf2f..0000000 --- a/tocin-web/Cargo.toml +++ /dev/null @@ -1,50 +0,0 @@ -[package] -authors = ["Juan Alvarez "] -categories = ["network-programming", "asynchronous"] -description = """ -grpc-web protocol translation for tonic services. -""" -edition = "2021" -homepage = "https://github.com/hyperium/tonic" -keywords = ["rpc", "grpc", "grpc-web"] -license = "MIT" -name = "tocin-web" -readme = "README.md" -repository = "https://github.com/hyperium/tonic" -version = "0.14.1" -rust-version = { workspace = true } - -[dependencies] -base64 = "0.22" -bytes = "1" -tokio-stream = { version = "0.1", default-features = false } -http = "1" -http-body = "1" -pin-project = "1" -tocin = { version = "0.14.0", path = "../tocin", default-features = false } -tower-service = "0.3" -tower-layer = "0.3" -tracing = "0.1" - -[dev-dependencies] -tokio = { version = "1", features = ["macros", "rt"] } -tower-http = { version = "0.6", features = ["cors"] } -axum = { version = "0.8", default-features = false } - -[lints] -workspace = true - -[package.metadata.cargo_check_external_types] -allowed_external_types = [ - "tonic::*", - - # major released - "bytes::*", - "http::*", - "http_body::*", - - # not major released - "futures_core::stream::Stream", - "tower_layer::Layer", - "tower_service::Service", -] diff --git a/tocin-web/LICENSE b/tocin-web/LICENSE deleted file mode 120000 index ea5b606..0000000 --- a/tocin-web/LICENSE +++ /dev/null @@ -1 +0,0 @@ -../LICENSE \ No newline at end of file diff --git a/tocin-web/README.md b/tocin-web/README.md deleted file mode 100644 index 110e30b..0000000 --- a/tocin-web/README.md +++ /dev/null @@ -1,32 +0,0 @@ -# tonic-web - -Enables tonic servers to handle requests from `grpc-web` clients directly, -without the need of an external proxy. - -## Enabling tonic services - -The easiest way to get started, is to call the function with your tonic service -and allow the tonic server to accept HTTP/1.1 requests: - -```rust -#[tokio::main] -async fn main() -> Result<(), Box> { - let addr = "[::1]:50051".parse().unwrap(); - let greeter = GreeterServer::new(MyGreeter::default()); - - Server::builder() - .accept_http1(true) - .layer(GrpcWebLayer::new()) - .add_service(greeter) - .serve(addr) - .await?; - - Ok(()) -} -``` - -## Examples - -See [the examples folder][example] for a server and client example. - -[example]: https://github.com/hyperium/tonic/tree/master/examples/src/grpc-web diff --git a/tocin-web/src/call.rs b/tocin-web/src/call.rs deleted file mode 100644 index 852c1e8..0000000 --- a/tocin-web/src/call.rs +++ /dev/null @@ -1,662 +0,0 @@ -use std::fmt; -use std::pin::Pin; -use std::task::{ready, Context, Poll}; - -use base64::Engine as _; -use bytes::{Buf, BufMut, Bytes, BytesMut}; -use http::{header, HeaderMap, HeaderName, HeaderValue}; -use http_body::{Body, Frame, SizeHint}; -use pin_project::pin_project; -use tokio_stream::Stream; -use tocin::Status; - -use self::content_types::*; - -// A grpc header is u8 (flag) + u32 (msg len) -const GRPC_HEADER_SIZE: usize = 1 + 4; - -pub(crate) mod content_types { - use http::{header::CONTENT_TYPE, HeaderMap}; - - pub(crate) const GRPC_WEB: &str = "application/grpc-web"; - pub(crate) const GRPC_WEB_PROTO: &str = "application/grpc-web+proto"; - pub(crate) const GRPC_WEB_TEXT: &str = "application/grpc-web-text"; - pub(crate) const GRPC_WEB_TEXT_PROTO: &str = "application/grpc-web-text+proto"; - - pub(crate) fn is_grpc_web(headers: &HeaderMap) -> bool { - matches!( - content_type(headers), - Some(GRPC_WEB) | Some(GRPC_WEB_PROTO) | Some(GRPC_WEB_TEXT) | Some(GRPC_WEB_TEXT_PROTO) - ) - } - - fn content_type(headers: &HeaderMap) -> Option<&str> { - headers.get(CONTENT_TYPE).and_then(|val| val.to_str().ok()) - } -} - -const BUFFER_SIZE: usize = 8 * 1024; - -const FRAME_HEADER_SIZE: usize = 5; - -// 8th (MSB) bit of the 1st gRPC frame byte -// denotes an uncompressed trailer (as part of the body) -const GRPC_WEB_TRAILERS_BIT: u8 = 0b10000000; - -#[derive(Copy, Clone, PartialEq, Debug)] -enum Direction { - Decode, - Encode, - Empty, -} - -#[derive(Copy, Clone, PartialEq, Debug)] -pub(crate) enum Encoding { - Base64, - None, -} - -/// HttpBody adapter for the grpc web based services. -#[derive(Debug)] -#[pin_project] -pub struct GrpcWebCall { - #[pin] - inner: B, - buf: BytesMut, - decoded: BytesMut, - direction: Direction, - encoding: Encoding, - client: bool, - trailers: Option, -} - -impl Default for GrpcWebCall { - fn default() -> Self { - Self { - inner: Default::default(), - buf: Default::default(), - decoded: Default::default(), - direction: Direction::Empty, - encoding: Encoding::None, - client: Default::default(), - trailers: Default::default(), - } - } -} - -impl GrpcWebCall { - pub(crate) fn request(inner: B, encoding: Encoding) -> Self { - Self::new(inner, Direction::Decode, encoding) - } - - pub(crate) fn response(inner: B, encoding: Encoding) -> Self { - Self::new(inner, Direction::Encode, encoding) - } - - pub(crate) fn client_request(inner: B) -> Self { - Self::new_client(inner, Direction::Encode, Encoding::None) - } - - pub(crate) fn client_response(inner: B) -> Self { - Self::new_client(inner, Direction::Decode, Encoding::None) - } - - fn new_client(inner: B, direction: Direction, encoding: Encoding) -> Self { - GrpcWebCall { - inner, - buf: BytesMut::with_capacity(match (direction, encoding) { - (Direction::Encode, Encoding::Base64) => BUFFER_SIZE, - _ => 0, - }), - decoded: BytesMut::with_capacity(match direction { - Direction::Decode => BUFFER_SIZE, - _ => 0, - }), - direction, - encoding, - client: true, - trailers: None, - } - } - - fn new(inner: B, direction: Direction, encoding: Encoding) -> Self { - GrpcWebCall { - inner, - buf: BytesMut::with_capacity(match (direction, encoding) { - (Direction::Encode, Encoding::Base64) => BUFFER_SIZE, - _ => 0, - }), - decoded: BytesMut::with_capacity(0), - direction, - encoding, - client: false, - trailers: None, - } - } - - // This is to avoid passing a slice of bytes with a length that the base64 - // decoder would consider invalid. - #[inline] - fn max_decodable(&self) -> usize { - (self.buf.len() / 4) * 4 - } - - fn decode_chunk(mut self: Pin<&mut Self>) -> Result, Status> { - // not enough bytes to decode - if self.buf.is_empty() || self.buf.len() < 4 { - return Ok(None); - } - - // Split `buf` at the largest index that is multiple of 4. Decode the - // returned `Bytes`, keeping the rest for the next attempt to decode. - let index = self.max_decodable(); - - crate::util::base64::STANDARD - .decode(self.as_mut().project().buf.split_to(index)) - .map(|decoded| Some(Bytes::from(decoded))) - .map_err(internal_error) - } -} - -impl GrpcWebCall -where - B: Body, - B::Data: Buf, - B::Error: fmt::Display, -{ - // Poll body for data, decoding (e.g. via Base64 if necessary) and returning frames - // to the caller. If the caller is a client, it should look for trailers before - // returning these frames. - fn poll_decode( - mut self: Pin<&mut Self>, - cx: &mut Context<'_>, - ) -> Poll, Status>>> { - match self.encoding { - Encoding::Base64 => loop { - if let Some(bytes) = self.as_mut().decode_chunk()? { - return Poll::Ready(Some(Ok(Frame::data(bytes)))); - } - - let this = self.as_mut().project(); - - match ready!(this.inner.poll_frame(cx)) { - Some(Ok(frame)) if frame.is_data() => this - .buf - .put(frame.into_data().unwrap_or_else(|_| unreachable!())), - Some(Ok(frame)) if frame.is_trailers() => { - return Poll::Ready(Some(Err(internal_error( - "malformed base64 request has unencoded trailers", - )))) - } - Some(Ok(_)) => { - return Poll::Ready(Some(Err(internal_error("unexpected frame type")))) - } - Some(Err(e)) => return Poll::Ready(Some(Err(internal_error(e)))), - None => { - return if this.buf.has_remaining() { - Poll::Ready(Some(Err(internal_error("malformed base64 request")))) - } else if let Some(trailers) = this.trailers.take() { - Poll::Ready(Some(Ok(Frame::trailers(trailers)))) - } else { - Poll::Ready(None) - } - } - } - }, - - Encoding::None => self - .project() - .inner - .poll_frame(cx) - .map_ok(|f| f.map_data(|mut d| d.copy_to_bytes(d.remaining()))) - .map_err(internal_error), - } - } - - fn poll_encode( - mut self: Pin<&mut Self>, - cx: &mut Context<'_>, - ) -> Poll, Status>>> { - let this = self.as_mut().project(); - - match ready!(this.inner.poll_frame(cx)) { - Some(Ok(frame)) if frame.is_data() => { - let mut data = frame.into_data().unwrap_or_else(|_| unreachable!()); - let mut res = data.copy_to_bytes(data.remaining()); - - if *this.encoding == Encoding::Base64 { - res = crate::util::base64::STANDARD.encode(res).into(); - } - - Poll::Ready(Some(Ok(Frame::data(res)))) - } - Some(Ok(frame)) if frame.is_trailers() => { - let trailers = frame.into_trailers().unwrap_or_else(|_| unreachable!()); - let mut res = make_trailers_frame(trailers); - - if *this.encoding == Encoding::Base64 { - res = crate::util::base64::STANDARD.encode(res).into(); - } - - Poll::Ready(Some(Ok(Frame::data(res)))) - } - Some(Ok(_)) => Poll::Ready(Some(Err(internal_error("unexpected frame type")))), - Some(Err(e)) => Poll::Ready(Some(Err(internal_error(e)))), - None => Poll::Ready(None), - } - } -} - -impl Body for GrpcWebCall -where - B: Body, - B::Error: fmt::Display, -{ - type Data = Bytes; - type Error = Status; - - fn poll_frame( - mut self: Pin<&mut Self>, - cx: &mut Context<'_>, - ) -> Poll, Self::Error>>> { - if self.client && self.direction == Direction::Decode { - let mut me = self.as_mut(); - - loop { - match ready!(me.as_mut().poll_decode(cx)) { - Some(Ok(incoming_buf)) if incoming_buf.is_data() => { - me.as_mut() - .project() - .decoded - .put(incoming_buf.into_data().unwrap()); - } - Some(Ok(incoming_buf)) if incoming_buf.is_trailers() => { - let trailers = incoming_buf.into_trailers().unwrap(); - match me.as_mut().project().trailers { - Some(current_trailers) => { - current_trailers.extend(trailers); - } - None => { - me.as_mut().project().trailers.replace(trailers); - } - } - continue; - } - Some(Ok(_)) => unreachable!("unexpected frame type"), - None => {} // No more data to decode, time to look for trailers - Some(Err(e)) => return Poll::Ready(Some(Err(e))), - }; - - // Hold the incoming, decoded data until we have a full message - // or trailers to return. - let buf = me.as_mut().project().decoded; - - return match find_trailers(&buf[..])? { - FindTrailers::Trailer(len) => { - // Extract up to len of where the trailers are at - let msg_buf = buf.copy_to_bytes(len); - match decode_trailers_frame(buf.split().freeze()) { - Ok(Some(trailers)) => { - me.as_mut().project().trailers.replace(trailers); - } - Err(e) => return Poll::Ready(Some(Err(e))), - _ => {} - } - - if msg_buf.has_remaining() { - Poll::Ready(Some(Ok(Frame::data(msg_buf)))) - } else if let Some(trailers) = me.as_mut().project().trailers.take() { - Poll::Ready(Some(Ok(Frame::trailers(trailers)))) - } else { - Poll::Ready(None) - } - } - FindTrailers::IncompleteBuf => continue, - FindTrailers::Done(len) => Poll::Ready(match len { - 0 => None, - _ => Some(Ok(Frame::data(buf.split_to(len).freeze()))), - }), - }; - } - } - - match self.direction { - Direction::Decode => self.poll_decode(cx), - Direction::Encode => self.poll_encode(cx), - Direction::Empty => Poll::Ready(None), - } - } - - fn is_end_stream(&self) -> bool { - self.inner.is_end_stream() - } - - fn size_hint(&self) -> SizeHint { - self.inner.size_hint() - } -} - -impl Stream for GrpcWebCall -where - B: Body, - B::Error: fmt::Display, -{ - type Item = Result, Status>; - - fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - self.poll_frame(cx) - } -} - -impl Encoding { - pub(crate) fn from_content_type(headers: &HeaderMap) -> Encoding { - Self::from_header(headers.get(header::CONTENT_TYPE)) - } - - pub(crate) fn from_accept(headers: &HeaderMap) -> Encoding { - Self::from_header(headers.get(header::ACCEPT)) - } - - pub(crate) fn to_content_type(self) -> &'static str { - match self { - Encoding::Base64 => GRPC_WEB_TEXT_PROTO, - Encoding::None => GRPC_WEB_PROTO, - } - } - - fn from_header(value: Option<&HeaderValue>) -> Encoding { - match value.and_then(|val| val.to_str().ok()) { - Some(GRPC_WEB_TEXT_PROTO) | Some(GRPC_WEB_TEXT) => Encoding::Base64, - _ => Encoding::None, - } - } -} - -fn internal_error(e: impl std::fmt::Display) -> Status { - Status::internal(format!("tonic-web: {e}")) -} - -// Key-value pairs encoded as a HTTP/1 headers block (without the terminating newline) -fn encode_trailers(trailers: HeaderMap) -> Vec { - trailers.iter().fold(Vec::new(), |mut acc, (key, value)| { - acc.put_slice(key.as_ref()); - acc.push(b':'); - acc.put_slice(value.as_bytes()); - acc.put_slice(b"\r\n"); - acc - }) -} - -fn decode_trailers_frame(mut buf: Bytes) -> Result, Status> { - if buf.remaining() < GRPC_HEADER_SIZE { - return Ok(None); - } - - buf.get_u8(); - buf.get_u32(); - - let mut map = HeaderMap::new(); - let mut temp_buf = buf.clone(); - - let mut trailers = Vec::new(); - let mut cursor_pos = 0; - - for (i, b) in buf.iter().enumerate() { - // if we are at a trailer delimiter (\r\n) - if b == &b'\r' && buf.get(i + 1) == Some(&b'\n') { - // read the bytes of the trailer passed so far - let trailer = temp_buf.copy_to_bytes(i - cursor_pos); - // increment cursor beyond the delimiter - cursor_pos = i + 2; - trailers.push(trailer); - if temp_buf.has_remaining() { - // advance buf beyond the delimiters - temp_buf.get_u8(); - temp_buf.get_u8(); - } - } - } - - for trailer in trailers { - let mut s = trailer.split(|b| b == &b':'); - let key = s - .next() - .ok_or_else(|| Status::internal("trailers couldn't parse key"))?; - let value = s - .next() - .ok_or_else(|| Status::internal("trailers couldn't parse value"))?; - - let value = value - .split(|b| b == &b'\r') - .next() - .ok_or_else(|| Status::internal("trailers was not escaped"))? - .strip_prefix(b" ") - .unwrap_or(value); - - let header_key = HeaderName::try_from(key) - .map_err(|e| Status::internal(format!("Unable to parse HeaderName: {e}")))?; - let header_value = HeaderValue::try_from(value) - .map_err(|e| Status::internal(format!("Unable to parse HeaderValue: {e}")))?; - map.insert(header_key, header_value); - } - - Ok(Some(map)) -} - -fn make_trailers_frame(trailers: HeaderMap) -> Bytes { - let trailers = encode_trailers(trailers); - let len = trailers.len(); - assert!(len <= u32::MAX as usize); - - let mut frame = BytesMut::with_capacity(len + FRAME_HEADER_SIZE); - frame.put_u8(GRPC_WEB_TRAILERS_BIT); - frame.put_u32(len as u32); - frame.put_slice(&trailers); - - frame.freeze() -} - -/// Search some buffer for grpc-web trailers headers and return -/// its location in the original buf. If `None` is returned we did -/// not find a trailers in this buffer either because its incomplete -/// or the buffer just contained grpc message frames. -fn find_trailers(buf: &[u8]) -> Result { - let mut len = 0; - let mut temp_buf = buf; - - loop { - // To check each frame, there must be at least GRPC_HEADER_SIZE - // amount of bytes available otherwise the buffer is incomplete. - if temp_buf.is_empty() || temp_buf.len() < GRPC_HEADER_SIZE { - return Ok(FindTrailers::Done(len)); - } - - let header = temp_buf.get_u8(); - - if header == GRPC_WEB_TRAILERS_BIT { - return Ok(FindTrailers::Trailer(len)); - } - - if !(header == 0 || header == 1) { - return Err(Status::internal(format!( - "Invalid header bit {header} expected 0 or 1" - ))); - } - - let msg_len = temp_buf.get_u32(); - - len += msg_len as usize + 4 + 1; - - // If the msg len of a non-grpc-web trailer frame is larger than - // the overall buffer we know within that buffer there are no trailers. - if len > buf.len() { - return Ok(FindTrailers::IncompleteBuf); - } - - temp_buf = &buf[len..]; - } -} - -#[derive(Debug, PartialEq, Eq)] -enum FindTrailers { - Trailer(usize), - IncompleteBuf, - Done(usize), -} - -#[cfg(test)] -mod tests { - use tocin::Code; - - use super::*; - - #[test] - fn encoding_constructors() { - let cases = &[ - (GRPC_WEB, Encoding::None), - (GRPC_WEB_PROTO, Encoding::None), - (GRPC_WEB_TEXT, Encoding::Base64), - (GRPC_WEB_TEXT_PROTO, Encoding::Base64), - ("foo", Encoding::None), - ]; - - let mut headers = HeaderMap::new(); - - for case in cases { - headers.insert(header::CONTENT_TYPE, case.0.parse().unwrap()); - headers.insert(header::ACCEPT, case.0.parse().unwrap()); - - assert_eq!(Encoding::from_content_type(&headers), case.1, "{}", case.0); - assert_eq!(Encoding::from_accept(&headers), case.1, "{}", case.0); - } - } - - #[test] - fn decode_trailers() { - let mut headers = HeaderMap::new(); - headers.insert(Status::GRPC_STATUS, 0.into()); - headers.insert( - Status::GRPC_MESSAGE, - "this is a message".try_into().unwrap(), - ); - - let trailers = make_trailers_frame(headers.clone()); - - let map = decode_trailers_frame(trailers).unwrap().unwrap(); - - assert_eq!(headers, map); - } - - #[test] - fn find_trailers_non_buffered() { - // Byte version of this: - // b"\x80\0\0\0\x0fgrpc-status:0\r\n" - let buf = [ - 128, 0, 0, 0, 15, 103, 114, 112, 99, 45, 115, 116, 97, 116, 117, 115, 58, 48, 13, 10, - ]; - - let out = find_trailers(&buf[..]).unwrap(); - - assert_eq!(out, FindTrailers::Trailer(0)); - } - - #[test] - fn find_trailers_buffered() { - // Byte version of this: - // b"\0\0\0\0L\n$975738af-1a17-4aea-b887-ed0bbced6093\x1a$da609e9b-f470-4cc0-a691-3fd6a005a436\x80\0\0\0\x0fgrpc-status:0\r\n" - let buf = [ - 0, 0, 0, 0, 76, 10, 36, 57, 55, 53, 55, 51, 56, 97, 102, 45, 49, 97, 49, 55, 45, 52, - 97, 101, 97, 45, 98, 56, 56, 55, 45, 101, 100, 48, 98, 98, 99, 101, 100, 54, 48, 57, - 51, 26, 36, 100, 97, 54, 48, 57, 101, 57, 98, 45, 102, 52, 55, 48, 45, 52, 99, 99, 48, - 45, 97, 54, 57, 49, 45, 51, 102, 100, 54, 97, 48, 48, 53, 97, 52, 51, 54, 128, 0, 0, 0, - 15, 103, 114, 112, 99, 45, 115, 116, 97, 116, 117, 115, 58, 48, 13, 10, - ]; - - let out = find_trailers(&buf[..]).unwrap(); - - assert_eq!(out, FindTrailers::Trailer(81)); - - let trailers = decode_trailers_frame(Bytes::copy_from_slice(&buf[81..])) - .unwrap() - .unwrap(); - let status = trailers.get(Status::GRPC_STATUS).unwrap(); - assert_eq!(status.to_str().unwrap(), "0") - } - - #[test] - fn find_trailers_buffered_incomplete_message() { - let buf = vec![ - 0, 0, 0, 9, 238, 10, 233, 19, 18, 230, 19, 10, 9, 10, 1, 120, 26, 4, 84, 69, 88, 84, - 18, 60, 10, 58, 10, 56, 3, 0, 0, 0, 44, 0, 0, 0, 0, 0, 0, 0, 116, 104, 105, 115, 32, - 118, 97, 108, 117, 101, 32, 119, 97, 115, 32, 119, 114, 105, 116, 116, 101, 110, 32, - 118, 105, 97, 32, 119, 114, 105, 116, 101, 32, 100, 101, 108, 101, 103, 97, 116, 105, - 111, 110, 33, 18, 62, 10, 60, 10, 58, 3, 0, 0, 0, 46, 0, 0, 0, 0, 0, 0, 0, 116, 104, - 105, 115, 32, 118, 97, 108, 117, 101, 32, 119, 97, 115, 32, 119, 114, 105, 116, 116, - 101, 110, 32, 98, 121, 32, 97, 110, 32, 101, 109, 98, 101, 100, 100, 101, 100, 32, 114, - 101, 112, 108, 105, 99, 97, 33, 18, 62, 10, 60, 10, 58, 3, 0, 0, 0, 46, 0, 0, 0, 0, 0, - 0, 0, 116, 104, 105, 115, 32, 118, 97, 108, 117, 101, 32, 119, 97, 115, 32, 119, 114, - 105, 116, 116, 101, 110, 32, 98, 121, 32, 97, 110, 32, 101, 109, 98, 101, 100, 100, - 101, 100, 32, 114, 101, 112, 108, 105, 99, 97, 33, 18, 62, 10, 60, 10, 58, 3, 0, 0, 0, - 46, 0, 0, 0, 0, 0, 0, 0, 116, 104, 105, 115, 32, 118, 97, 108, 117, 101, 32, 119, 97, - 115, 32, 119, 114, 105, 116, 116, 101, 110, 32, 98, 121, 32, 97, 110, 32, 101, 109, 98, - 101, 100, 100, 101, 100, 32, 114, 101, 112, 108, 105, 99, 97, 33, 18, 62, 10, 60, 10, - 58, 3, 0, 0, 0, 46, 0, 0, 0, 0, 0, 0, 0, 116, 104, 105, 115, 32, 118, 97, 108, 117, - 101, 32, 119, 97, 115, 32, 119, 114, 105, 116, 116, 101, 110, 32, 98, 121, 32, 97, 110, - 32, 101, 109, 98, 101, 100, 100, 101, 100, 32, 114, 101, 112, 108, 105, 99, 97, 33, 18, - 62, 10, 60, 10, 58, 3, 0, 0, 0, 46, 0, 0, 0, 0, 0, 0, 0, 116, 104, 105, 115, 32, 118, - 97, 108, 117, 101, 32, 119, 97, 115, 32, 119, 114, 105, 116, 116, 101, 110, 32, 98, - 121, 32, 97, 110, 32, 101, 109, 98, 101, 100, 100, 101, 100, 32, 114, 101, 112, 108, - 105, 99, 97, 33, 18, 62, 10, 60, 10, 58, 3, 0, 0, 0, 46, 0, 0, 0, 0, 0, 0, 0, 116, 104, - 105, 115, 32, 118, 97, 108, 117, 101, 32, 119, 97, 115, 32, 119, 114, 105, 116, 116, - 101, 110, 32, 98, 121, 32, 97, 110, 32, 101, 109, 98, 101, 100, 100, 101, 100, 32, 114, - 101, 112, 108, 105, 99, 97, 33, 18, 62, 10, 60, 10, 58, 3, 0, 0, 0, 46, 0, 0, 0, 0, 0, - 0, 0, 116, 104, 105, 115, 32, 118, 97, 108, 117, 101, 32, 119, 97, 115, 32, 119, 114, - 105, 116, 116, 101, 110, 32, 98, 121, 32, - ]; - - let out = find_trailers(&buf[..]).unwrap(); - - assert_eq!(out, FindTrailers::IncompleteBuf); - } - - #[test] - #[ignore] - fn find_trailers_buffered_incomplete_buf_bug() { - let buf = std::fs::read("tests/incomplete-buf-bug.bin").unwrap(); - let out = find_trailers(&buf[..]).unwrap_err(); - - assert_eq!(out.code(), Code::Internal); - } - - #[test] - fn decode_multiple_trailers() { - let buf = b"\x80\0\0\0\x0fgrpc-status:0\r\ngrpc-message:\r\na:1\r\nb:2\r\n"; - - let trailers = decode_trailers_frame(Bytes::copy_from_slice(&buf[..])) - .unwrap() - .unwrap(); - - let mut expected = HeaderMap::new(); - expected.insert(Status::GRPC_STATUS, "0".parse().unwrap()); - expected.insert(Status::GRPC_MESSAGE, "".parse().unwrap()); - expected.insert("a", "1".parse().unwrap()); - expected.insert("b", "2".parse().unwrap()); - - assert_eq!(trailers, expected); - } - - #[test] - fn decode_trailers_with_space_after_colon() { - let buf = b"\x80\0\0\0\x0fgrpc-status: 0\r\ngrpc-message: \r\n"; - - let trailers = decode_trailers_frame(Bytes::copy_from_slice(&buf[..])) - .unwrap() - .unwrap(); - - let mut expected = HeaderMap::new(); - expected.insert(Status::GRPC_STATUS, "0".parse().unwrap()); - expected.insert(Status::GRPC_MESSAGE, "".parse().unwrap()); - - assert_eq!(trailers, expected); - } -} diff --git a/tocin-web/src/client.rs b/tocin-web/src/client.rs deleted file mode 100644 index 140d26b..0000000 --- a/tocin-web/src/client.rs +++ /dev/null @@ -1,106 +0,0 @@ -use http::header::CONTENT_TYPE; -use http::{Request, Response, Version}; -use pin_project::pin_project; -use std::fmt; -use std::future::Future; -use std::pin::Pin; -use std::task::{ready, Context, Poll}; -use tower_layer::Layer; -use tower_service::Service; -use tracing::debug; - -use crate::call::content_types::GRPC_WEB; -use crate::call::GrpcWebCall; - -/// Layer implementing the grpc-web protocol for clients. -#[derive(Debug, Default, Clone)] -pub struct GrpcWebClientLayer { - _priv: (), -} - -impl GrpcWebClientLayer { - /// Create a new grpc-web for clients layer. - pub fn new() -> GrpcWebClientLayer { - Self::default() - } -} - -impl Layer for GrpcWebClientLayer { - type Service = GrpcWebClientService; - - fn layer(&self, inner: S) -> Self::Service { - GrpcWebClientService::new(inner) - } -} - -/// A [`Service`] that wraps some inner http service that will -/// coerce requests coming from [`tocin::client::Grpc`] into proper -/// `grpc-web` requests. -#[derive(Debug, Clone)] -pub struct GrpcWebClientService { - inner: S, -} - -impl GrpcWebClientService { - /// Create a new grpc-web for clients service. - pub fn new(inner: S) -> Self { - Self { inner } - } -} - -impl Service> for GrpcWebClientService -where - S: Service>, Response = Response>, -{ - type Response = Response>; - type Error = S::Error; - type Future = ResponseFuture; - - fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { - self.inner.poll_ready(cx) - } - - fn call(&mut self, mut req: Request) -> Self::Future { - if req.version() == Version::HTTP_2 { - debug!("coercing HTTP2 request to HTTP1.1"); - - *req.version_mut() = Version::HTTP_11; - } - - req.headers_mut() - .insert(CONTENT_TYPE, GRPC_WEB.try_into().unwrap()); - - let req = req.map(GrpcWebCall::client_request); - - let fut = self.inner.call(req); - - ResponseFuture { inner: fut } - } -} - -/// Response future for the [`GrpcWebService`](crate::GrpcWebService). -#[pin_project] -#[must_use = "futures do nothing unless polled"] -pub struct ResponseFuture { - #[pin] - inner: F, -} - -impl Future for ResponseFuture -where - F: Future, E>>, -{ - type Output = Result>, E>; - - fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { - let res = ready!(self.project().inner.poll(cx)); - - Poll::Ready(res.map(|r| r.map(GrpcWebCall::client_response))) - } -} - -impl fmt::Debug for ResponseFuture { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("ResponseFuture").finish() - } -} diff --git a/tocin-web/src/layer.rs b/tocin-web/src/layer.rs deleted file mode 100644 index ac171af..0000000 --- a/tocin-web/src/layer.rs +++ /dev/null @@ -1,24 +0,0 @@ -use super::GrpcWebService; - -use tower_layer::Layer; - -/// Layer implementing the grpc-web protocol. -#[derive(Debug, Default, Clone)] -pub struct GrpcWebLayer { - _priv: (), -} - -impl GrpcWebLayer { - /// Create a new grpc-web layer. - pub fn new() -> GrpcWebLayer { - Self::default() - } -} - -impl Layer for GrpcWebLayer { - type Service = GrpcWebService; - - fn layer(&self, inner: S) -> Self::Service { - GrpcWebService::new(inner) - } -} diff --git a/tocin-web/src/lib.rs b/tocin-web/src/lib.rs deleted file mode 100644 index c7859e7..0000000 --- a/tocin-web/src/lib.rs +++ /dev/null @@ -1,102 +0,0 @@ -//! grpc-web protocol translation for [`tonic`] services. -//! -//! [`tocin_web`] enables tonic servers to handle requests from [grpc-web] clients directly, -//! without the need of an external proxy. It achieves this by wrapping individual tonic services -//! with a [tower] service that performs the translation between protocols and handles `cors` -//! requests. -//! -//! ## Enabling tonic services -//! -//! You can customize the CORS configuration composing the [`GrpcWebLayer`] with the cors layer of your choice. -//! -//! ```ignore -//! #[tokio::main] -//! async fn main() -> Result<(), Box> { -//! let addr = "[::1]:50051".parse().unwrap(); -//! let greeter = GreeterServer::new(MyGreeter::default()); -//! -//! Server::builder() -//! .accept_http1(true) -//! // This will apply the gRPC-Web translation layer -//! .layer(GrpcWebLayer::new()) -//! .add_service(greeter) -//! .serve(addr) -//! .await?; -//! -//! Ok(()) -//! } -//! ``` -//! -//! Alternatively, if you have a tls enabled server, you could skip setting `accept_http1` to `true`. -//! This works because the browser will handle `ALPN`. -//! -//! ```ignore -//! #[tokio::main] -//! async fn main() -> Result<(), Box> { -//! let cert = tokio::fs::read("server.pem").await?; -//! let key = tokio::fs::read("server.key").await?; -//! let identity = Identity::from_pem(cert, key); -//! -//! let addr = "[::1]:50051".parse().unwrap(); -//! let greeter = GreeterServer::new(MyGreeter::default()); -//! -//! // No need to enable HTTP/1 -//! Server::builder() -//! .tls_config(ServerTlsConfig::new().identity(identity))? -//! .layer(GrpcWebLayer::new()) -//! .add_service(greeter) -//! .serve(addr) -//! .await?; -//! -//! Ok(()) -//! } -//! ``` -//! -//! ## Limitations -//! -//! * `tocin_web` is designed to work with grpc-web-compliant clients only. It is not expected to -//! handle arbitrary HTTP/x.x requests or bespoke protocols. -//! * Similarly, the cors support implemented by this crate will *only* handle grpc-web and -//! grpc-web preflight requests. -//! * Currently, grpc-web clients can only perform `unary` and `server-streaming` calls. These -//! are the only requests this crate is designed to handle. Support for client and bi-directional -//! streaming will be officially supported when clients do. -//! * There is no support for web socket transports. -//! -//! -//! [`tonic`]: https://github.com/hyperium/tonic -//! [`tocin_web`]: https://github.com/hyperium/tonic -//! [grpc-web]: https://github.com/grpc/grpc-web -//! [tower]: https://github.com/tower-rs/tower -#![doc(issue_tracker_base_url = "https://github.com/hyperium/tonic/issues/")] - -pub use call::GrpcWebCall; -pub use client::{GrpcWebClientLayer, GrpcWebClientService}; -pub use layer::GrpcWebLayer; -pub use service::{GrpcWebService, ResponseFuture}; - -mod call; -mod client; -mod layer; -mod service; - -type BoxError = Box; - -pub(crate) mod util { - pub(crate) mod base64 { - use base64::{ - alphabet, - engine::{ - general_purpose::{GeneralPurpose, GeneralPurposeConfig}, - DecodePaddingMode, - }, - }; - - pub(crate) const STANDARD: GeneralPurpose = GeneralPurpose::new( - &alphabet::STANDARD, - GeneralPurposeConfig::new() - .with_encode_padding(true) - .with_decode_padding_mode(DecodePaddingMode::Indifferent), - ); - } -} diff --git a/tocin-web/src/service.rs b/tocin-web/src/service.rs deleted file mode 100644 index adec97f..0000000 --- a/tocin-web/src/service.rs +++ /dev/null @@ -1,485 +0,0 @@ -use core::fmt; -use std::future::Future; -use std::pin::Pin; -use std::task::{ready, Context, Poll}; - -use http::{header, HeaderMap, HeaderValue, Method, Request, Response, StatusCode, Version}; -use pin_project::pin_project; -use tocin::metadata::GRPC_CONTENT_TYPE; -use tocin::{body::Body, server::NamedService}; -use tower_service::Service; -use tracing::{debug, trace}; - -use crate::call::content_types::is_grpc_web; -use crate::call::{Encoding, GrpcWebCall}; - -/// Service implementing the grpc-web protocol. -#[derive(Debug, Clone)] -pub struct GrpcWebService { - inner: S, -} - -#[derive(Debug, PartialEq)] -enum RequestKind<'a> { - // The request is considered a grpc-web request if its `content-type` - // header is exactly one of: - // - // - "application/grpc-web" - // - "application/grpc-web+proto" - // - "application/grpc-web-text" - // - "application/grpc-web-text+proto" - GrpcWeb { - method: &'a Method, - encoding: Encoding, - accept: Encoding, - }, - // All other requests, including `application/grpc` - Other(http::Version), -} - -impl GrpcWebService { - pub(crate) fn new(inner: S) -> Self { - GrpcWebService { inner } - } -} - -impl Service> for GrpcWebService -where - S: Service, Response = Response>, - ReqBody: http_body::Body + Send + 'static, - ReqBody::Error: Into + fmt::Display, - ResBody: http_body::Body + Send + 'static, - ResBody::Error: Into + fmt::Display, -{ - type Response = Response; - type Error = S::Error; - type Future = ResponseFuture; - - fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { - self.inner.poll_ready(cx) - } - - fn call(&mut self, req: Request) -> Self::Future { - match RequestKind::new(req.headers(), req.method(), req.version()) { - // A valid grpc-web request, regardless of HTTP version. - // - // If the request includes an `origin` header, we verify it is allowed - // to access the resource, an HTTP 403 response is returned otherwise. - // - // If the origin is allowed to access the resource or there is no - // `origin` header present, translate the request into a grpc request, - // call the inner service, and translate the response back to - // grpc-web. - RequestKind::GrpcWeb { - method: &Method::POST, - encoding, - accept, - } => { - trace!(kind = "simple", path = ?req.uri().path(), ?encoding, ?accept); - - ResponseFuture { - case: Case::GrpcWeb { - future: self.inner.call(coerce_request(req, encoding)), - accept, - }, - } - } - - // The request's content-type matches one of the 4 supported grpc-web - // content-types, but the request method is not `POST`. - // This is not a valid grpc-web request, return HTTP 405. - RequestKind::GrpcWeb { .. } => { - debug!(kind = "simple", error="method not allowed", method = ?req.method()); - - ResponseFuture { - case: Case::immediate(StatusCode::METHOD_NOT_ALLOWED), - } - } - - // All http/2 requests that are not grpc-web are passed through to the inner service, - // whatever they are. - RequestKind::Other(Version::HTTP_2) => { - debug!(kind = "other h2", content_type = ?req.headers().get(header::CONTENT_TYPE)); - ResponseFuture { - case: Case::Other { - future: self.inner.call(req.map(Body::new)), - }, - } - } - - // Return HTTP 400 for all other requests. - RequestKind::Other(_) => { - debug!(kind = "other h1", content_type = ?req.headers().get(header::CONTENT_TYPE)); - - ResponseFuture { - case: Case::immediate(StatusCode::BAD_REQUEST), - } - } - } - } -} - -/// Response future for the [`GrpcWebService`]. -#[pin_project] -#[must_use = "futures do nothing unless polled"] -pub struct ResponseFuture { - #[pin] - case: Case, -} - -#[pin_project(project = CaseProj)] -enum Case { - GrpcWeb { - #[pin] - future: F, - accept: Encoding, - }, - Other { - #[pin] - future: F, - }, - ImmediateResponse { - res: Option, - }, -} - -impl Case { - fn immediate(status: StatusCode) -> Self { - let (res, ()) = Response::builder() - .status(status) - .body(()) - .unwrap() - .into_parts(); - Self::ImmediateResponse { res: Some(res) } - } -} - -impl Future for ResponseFuture -where - F: Future, E>>, - B: http_body::Body + Send + 'static, - B::Error: Into + fmt::Display, -{ - type Output = Result, E>; - - fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { - let this = self.project(); - - match this.case.project() { - CaseProj::GrpcWeb { future, accept } => { - let res = ready!(future.poll(cx))?; - - Poll::Ready(Ok(coerce_response(res, *accept))) - } - CaseProj::Other { future } => future.poll(cx).map_ok(|res| res.map(Body::new)), - CaseProj::ImmediateResponse { res } => { - let res = Response::from_parts(res.take().unwrap(), Body::empty()); - Poll::Ready(Ok(res)) - } - } - } -} - -impl NamedService for GrpcWebService { - const NAME: &'static str = S::NAME; -} - -impl fmt::Debug for ResponseFuture { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("ResponseFuture").finish() - } -} - -impl<'a> RequestKind<'a> { - fn new(headers: &'a HeaderMap, method: &'a Method, version: Version) -> Self { - if is_grpc_web(headers) { - return RequestKind::GrpcWeb { - method, - encoding: Encoding::from_content_type(headers), - accept: Encoding::from_accept(headers), - }; - } - - RequestKind::Other(version) - } -} - -// Mutating request headers to conform to a gRPC request is not really -// necessary for us at this point. We could remove most of these except -// maybe for inserting `header::TE`, which tonic should check? -fn coerce_request(mut req: Request, encoding: Encoding) -> Request -where - B: http_body::Body + Send + 'static, - B::Error: Into + fmt::Display, -{ - req.headers_mut().remove(header::CONTENT_LENGTH); - - req.headers_mut() - .insert(header::CONTENT_TYPE, GRPC_CONTENT_TYPE); - - req.headers_mut() - .insert(header::TE, HeaderValue::from_static("trailers")); - - req.headers_mut().insert( - header::ACCEPT_ENCODING, - HeaderValue::from_static("identity,deflate,gzip"), - ); - - req.map(|b| Body::new(GrpcWebCall::request(b, encoding))) -} - -fn coerce_response(res: Response, encoding: Encoding) -> Response -where - B: http_body::Body + Send + 'static, - B::Error: Into + fmt::Display, -{ - let mut res = res - .map(|b| GrpcWebCall::response(b, encoding)) - .map(Body::new); - - res.headers_mut().insert( - header::CONTENT_TYPE, - HeaderValue::from_static(encoding.to_content_type()), - ); - - res -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::call::content_types::*; - use http::header::{ - ACCESS_CONTROL_REQUEST_HEADERS, ACCESS_CONTROL_REQUEST_METHOD, CONTENT_TYPE, ORIGIN, - }; - use tower_layer::Layer as _; - - type BoxFuture = Pin> + Send>>; - - #[derive(Debug, Clone)] - struct Svc; - - impl tower_service::Service> for Svc { - type Response = Response; - type Error = std::convert::Infallible; - type Future = BoxFuture; - - fn poll_ready(&mut self, _: &mut Context<'_>) -> Poll> { - Poll::Ready(Ok(())) - } - - fn call(&mut self, _: Request) -> Self::Future { - Box::pin(async { Ok(Response::new(Body::default())) }) - } - } - - impl NamedService for Svc { - const NAME: &'static str = "test"; - } - - fn enable(service: S) -> tower_http::cors::Cors> - where - S: Service, Response = http::Response>, - { - tower_layer::Stack::new( - crate::GrpcWebLayer::new(), - tower_http::cors::CorsLayer::new(), - ) - .layer(service) - } - - mod grpc_web { - use super::*; - use tower_layer::Layer; - - fn request() -> Request { - Request::builder() - .method(Method::POST) - .header(CONTENT_TYPE, GRPC_WEB) - .header(ORIGIN, "http://example.com") - .body(Body::default()) - .unwrap() - } - - #[tokio::test] - async fn default_cors_config() { - let mut svc = enable(Svc); - let res = svc.call(request()).await.unwrap(); - - assert_eq!(res.status(), StatusCode::OK); - } - - #[tokio::test] - async fn web_layer() { - let mut svc = crate::GrpcWebLayer::new().layer(Svc); - let res = svc.call(request()).await.unwrap(); - - assert_eq!(res.status(), StatusCode::OK); - } - - #[tokio::test] - async fn web_layer_with_axum() { - let mut svc = axum::routing::Router::new() - .route("/", axum::routing::post_service(Svc)) - .layer(crate::GrpcWebLayer::new()); - - let res = svc.call(request()).await.unwrap(); - - assert_eq!(res.status(), StatusCode::OK); - } - - #[tokio::test] - async fn without_origin() { - let mut svc = enable(Svc); - - let mut req = request(); - req.headers_mut().remove(ORIGIN); - - let res = svc.call(req).await.unwrap(); - - assert_eq!(res.status(), StatusCode::OK); - } - - #[tokio::test] - async fn only_post_and_options_allowed() { - let mut svc = enable(Svc); - - for method in &[ - Method::GET, - Method::PUT, - Method::DELETE, - Method::HEAD, - Method::PATCH, - ] { - let mut req = request(); - *req.method_mut() = method.clone(); - - let res = svc.call(req).await.unwrap(); - - assert_eq!( - res.status(), - StatusCode::METHOD_NOT_ALLOWED, - "{method} should not be allowed" - ); - } - } - - #[tokio::test] - async fn grpc_web_content_types() { - let mut svc = enable(Svc); - - for ct in &[GRPC_WEB_TEXT, GRPC_WEB_PROTO, GRPC_WEB_TEXT_PROTO, GRPC_WEB] { - let mut req = request(); - req.headers_mut() - .insert(CONTENT_TYPE, HeaderValue::from_static(ct)); - - let res = svc.call(req).await.unwrap(); - - assert_eq!(res.status(), StatusCode::OK); - } - } - } - - mod options { - use super::*; - - fn request() -> Request { - Request::builder() - .method(Method::OPTIONS) - .header(ORIGIN, "http://example.com") - .header(ACCESS_CONTROL_REQUEST_HEADERS, "x-grpc-web") - .header(ACCESS_CONTROL_REQUEST_METHOD, "POST") - .body(Body::default()) - .unwrap() - } - - #[tokio::test] - async fn valid_grpc_web_preflight() { - let mut svc = enable(Svc); - let res = svc.call(request()).await.unwrap(); - - assert_eq!(res.status(), StatusCode::OK); - } - } - - mod grpc { - use super::*; - - fn request() -> Request { - Request::builder() - .version(Version::HTTP_2) - .header(CONTENT_TYPE, GRPC_CONTENT_TYPE) - .body(Body::default()) - .unwrap() - } - - #[tokio::test] - async fn h2_is_ok() { - let mut svc = enable(Svc); - - let req = request(); - let res = svc.call(req).await.unwrap(); - - assert_eq!(res.status(), StatusCode::OK) - } - - #[tokio::test] - async fn h1_is_err() { - let mut svc = enable(Svc); - - let req = Request::builder() - .header(CONTENT_TYPE, GRPC_CONTENT_TYPE) - .body(Body::default()) - .unwrap(); - - let res = svc.call(req).await.unwrap(); - assert_eq!(res.status(), StatusCode::BAD_REQUEST) - } - - #[tokio::test] - async fn content_type_variants() { - let mut svc = enable(Svc); - - for variant in &["grpc", "grpc+proto", "grpc+thrift", "grpc+foo"] { - let mut req = request(); - req.headers_mut().insert( - CONTENT_TYPE, - HeaderValue::from_maybe_shared(format!("application/{variant}")).unwrap(), - ); - - let res = svc.call(req).await.unwrap(); - - assert_eq!(res.status(), StatusCode::OK) - } - } - } - - mod other { - use super::*; - - fn request() -> Request { - Request::builder() - .header(CONTENT_TYPE, "application/text") - .body(Body::default()) - .unwrap() - } - - #[tokio::test] - async fn h1_is_err() { - let mut svc = enable(Svc); - let res = svc.call(request()).await.unwrap(); - - assert_eq!(res.status(), StatusCode::BAD_REQUEST) - } - - #[tokio::test] - async fn h2_is_ok() { - let mut svc = enable(Svc); - let mut req = request(); - *req.version_mut() = Version::HTTP_2; - - let res = svc.call(req).await.unwrap(); - assert_eq!(res.status(), StatusCode::OK) - } - } -} diff --git a/tocin-web/tests/incomplete-buf-bug.bin b/tocin-web/tests/incomplete-buf-bug.bin deleted file mode 100644 index 19cb741..0000000 Binary files a/tocin-web/tests/incomplete-buf-bug.bin and /dev/null differ diff --git a/tocin/src/client/grpc.rs b/tocin/src/client/grpc.rs index 17c92b6..5c4b1c3 100644 --- a/tocin/src/client/grpc.rs +++ b/tocin/src/client/grpc.rs @@ -2,7 +2,7 @@ use crate::codec::EncodeBody; use crate::codec::{CompressionEncoding, EnabledCompressionEncodings}; use crate::metadata::GRPC_CONTENT_TYPE; use crate::{ - client::GrpcService, + client::{GrpcService, BoxBody}, codec::{Codec, Decoder, Streaming}, request::SanitizeHeaders, Code, Request, Response, Status, @@ -73,29 +73,6 @@ impl<'arena, T> Grpc<'arena, T> { /// Compress requests with the provided encoding. /// /// Requires the server to accept the specified encoding, otherwise it might return an error. - /// - /// # Example - /// - /// The most common way of using this is through a client generated by tonic-build: - /// - /// ```rust - /// use tocin::transport::Channel; - /// # enum CompressionEncoding { Gzip } - /// # struct TestClient(T); - /// # impl TestClient { - /// # fn new(channel: T) -> Self { Self(channel) } - /// # fn send_compressed(self, _: CompressionEncoding) -> Self { self } - /// # } - /// - /// # async { - /// let channel = Channel::builder("127.0.0.1:3000".parse().unwrap()) - /// .connect() - /// .await - /// .unwrap(); - /// - /// let client = TestClient::new(channel).send_compressed(CompressionEncoding::Gzip); - /// # }; - /// ``` pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self { self.config.send_compression_encodings = Some(encoding); self @@ -104,102 +81,31 @@ impl<'arena, T> Grpc<'arena, T> { /// Enable accepting compressed responses. /// /// Requires the server to also support sending compressed responses. - /// - /// # Example - /// - /// The most common way of using this is through a client generated by tonic-build: - /// - /// ```rust - /// use tocin::transport::Channel; - /// # enum CompressionEncoding { Gzip } - /// # struct TestClient(T); - /// # impl TestClient { - /// # fn new(channel: T) -> Self { Self(channel) } - /// # fn accept_compressed(self, _: CompressionEncoding) -> Self { self } - /// # } - /// - /// # async { - /// let channel = Channel::builder("127.0.0.1:3000".parse().unwrap()) - /// .connect() - /// .await - /// .unwrap(); - /// - /// let client = TestClient::new(channel).accept_compressed(CompressionEncoding::Gzip); - /// # }; - /// ``` pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self { self.config.accept_compression_encodings.enable(encoding); self } /// Limits the maximum size of a decoded message. - /// - /// # Example - /// - /// The most common way of using this is through a client generated by tonic-build: - /// - /// ```rust - /// use tocin::transport::Channel; - /// # struct TestClient(T); - /// # impl TestClient { - /// # fn new(channel: T) -> Self { Self(channel) } - /// # fn max_decoding_message_size(self, _: usize) -> Self { self } - /// # } - /// - /// # async { - /// let channel = Channel::builder("127.0.0.1:3000".parse().unwrap()) - /// .connect() - /// .await - /// .unwrap(); - /// - /// // Set the limit to 2MB, Defaults to 4MB. - /// let limit = 2 * 1024 * 1024; - /// let client = TestClient::new(channel).max_decoding_message_size(limit); - /// # }; - /// ``` pub fn max_decoding_message_size(mut self, limit: usize) -> Self { self.config.max_decoding_message_size = Some(limit); self } /// Limits the maximum size of an encoded message. - /// - /// # Example - /// - /// The most common way of using this is through a client generated by tonic-build: - /// - /// ```rust - /// use tocin::transport::Channel; - /// # struct TestClient(T); - /// # impl TestClient { - /// # fn new(channel: T) -> Self { Self(channel) } - /// # fn max_encoding_message_size(self, _: usize) -> Self { self } - /// # } - /// - /// # async { - /// let channel = Channel::builder("127.0.0.1:3000".parse().unwrap()) - /// .connect() - /// .await - /// .unwrap(); - /// - /// // Set the limit to 2MB, Defaults to `usize::MAX`. - /// let limit = 2 * 1024 * 1024; - /// let client = TestClient::new(channel).max_encoding_message_size(limit); - /// # }; - /// ``` pub fn max_encoding_message_size(mut self, limit: usize) -> Self { self.config.max_encoding_message_size = Some(limit); self } - /// Check if the inner [`GrpcService`] is able to accept a new request. + /// Check if the inner [`GrpcService`] is able to accept a new request. /// /// This will call [`GrpcService::poll_ready`] until it returns ready or /// an error. If this returns ready the inner [`GrpcService`] is ready to /// accept one more request. - pub async fn ready(&mut self) -> Result<(), T::Error> + pub async fn ready(&mut self) -> Result<(), T::Error> where - T: GrpcService, + T: GrpcService, { future::poll_fn(|cx| self.inner.poll_ready(cx)).await } @@ -212,8 +118,9 @@ impl<'arena, T> Grpc<'arena, T> { codec: C, ) -> Result, Status> where - T: GrpcService>, fn(M1) -> Result>>, ResponseBody = monoio_http::h2::RecvStream>, + T: GrpcService, C: Codec<'arena, Encode = M1, Decode = M2>, + M1: 'arena, M2: 'arena, { let request = request.map(|m| futures_util::stream::once(futures_util::future::ready(m))); @@ -228,8 +135,8 @@ impl<'arena, T> Grpc<'arena, T> { codec: C, ) -> Result, Status> where - T: GrpcService Result>>, ResponseBody = monoio_http::h2::RecvStream>, - S: Stream, + T: GrpcService, + S: Stream + Unpin + 'arena, C: Codec<'arena, Encode = M1, Decode = M2>, M2: 'arena, { @@ -262,8 +169,9 @@ impl<'arena, T> Grpc<'arena, T> { codec: C, ) -> Result>, Status> where - T: GrpcService>, fn(M1) -> Result>>, ResponseBody = monoio_http::h2::RecvStream>, + T: GrpcService, C: Codec<'arena, Encode = M1, Decode = M2>, + M1: 'arena, M2: 'arena, { let request = request.map(|m| futures_util::stream::once(futures_util::future::ready(m))); @@ -278,18 +186,20 @@ impl<'arena, T> Grpc<'arena, T> { mut codec: C, ) -> Result>, Status> where - T: GrpcService Result>>, ResponseBody = monoio_http::h2::RecvStream>, - S: Stream, + T: GrpcService, + S: Stream + Unpin + 'arena, C: Codec<'arena, Encode = M1, Decode = M2>, M2: 'arena, { let request = request.map(|s| { - EncodeBody::new_client( + let encode_body = EncodeBody::new_client( codec.encoder(), s.map(Ok as fn(M1) -> Result), self.config.send_compression_encodings, self.config.max_encoding_message_size, - ) + ); + // Box-erase the body type so GrpcService doesn't need to be generic + BoxBody::new(encode_body) }); let request = self.config.prepare_request(request, path); @@ -307,13 +217,12 @@ impl<'arena, T> Grpc<'arena, T> { // Keeping this code in a separate function from Self::streaming lets functions that return the // same output share the generated binary code - fn create_response( + fn create_response( &self, decoder: D, response: http::Response, ) -> Result>, Status> where - T: GrpcService, D: Decoder<'arena, Error = Status>, { let encoding = CompressionEncoding::from_encoding_header( diff --git a/tocin/src/client/mod.rs b/tocin/src/client/mod.rs index 815aa8e..99be751 100644 --- a/tocin/src/client/mod.rs +++ b/tocin/src/client/mod.rs @@ -18,4 +18,4 @@ mod grpc; mod service; pub use self::grpc::Grpc; -pub use self::service::GrpcService; +pub use self::service::{BoxBody, GrpcService}; diff --git a/tocin/src/client/service.rs b/tocin/src/client/service.rs index ad0ab79..a63a267 100644 --- a/tocin/src/client/service.rs +++ b/tocin/src/client/service.rs @@ -4,17 +4,59 @@ use std::task::{Context, Poll}; /// Definition of the gRPC service trait for clients. /// /// This trait defines the interface for sending gRPC requests over HTTP/2. -pub trait GrpcService { +/// The request body is type-erased via a `BoxBody` which wraps a stream +/// of bytes chunks. +pub trait GrpcService { /// Responses body given by the service. type ResponseBody: monoio_http::common::body::Body; /// Errors produced by the service. type Error: Into; - /// The future response value. - type Future: Future, Self::Error>>; /// Returns `Ready` when the service is able to process requests. fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll>; /// Process the request and return the response asynchronously. - fn call(&mut self, request: http::Request) -> Self::Future; + /// + /// The body is a stream of bytes chunks. The implementation is responsible + /// for draining the body into the HTTP/2 stream. + fn call<'a>( + &'a mut self, + request: http::Request>, + ) -> impl Future, Self::Error>> + 'a; } + +/// A type-erased body for client requests. +/// +/// This wraps any `Stream>` into a trait object, +/// so the `GrpcService` trait doesn't need to be generic over the body type. +/// +/// The lifetime `'a` allows the body to borrow from an arena or other +/// context — the body only needs to live until `GrpcService::call` drains it. +pub struct BoxBody<'a> { + inner: std::pin::Pin> + Unpin + 'a>>, +} + +impl<'a> BoxBody<'a> { + /// Create a new BoxBody wrapping a stream. + pub fn new(stream: S) -> Self + where + S: futures_core::Stream> + Unpin + 'a, + { + BoxBody { + inner: Box::pin(stream), + } + } +} + +impl<'a> futures_core::Stream for BoxBody<'a> { + type Item = Result; + + fn poll_next( + mut self: std::pin::Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll> { + self.inner.as_mut().poll_next(cx) + } +} + +impl<'a> Unpin for BoxBody<'a> {} diff --git a/tocin/src/codegen.rs b/tocin/src/codegen.rs index 3c080c4..b2c8d5b 100644 --- a/tocin/src/codegen.rs +++ b/tocin/src/codegen.rs @@ -8,11 +8,17 @@ pub use std::pin::Pin; pub use std::sync::Arc; pub use std::task::{Context, Poll}; -pub type StdError = Box; +pub type StdError = Box; pub use crate::codec::{CompressionEncoding, EnabledCompressionEncodings}; pub use crate::extensions::GrpcMethod; pub use bytes::Bytes; pub use http; +pub use monoio_http::common::body::Body; +pub use monoio_http::h2; +pub use monoio_http; + +// Re-export futures types needed by generated client code +pub use futures_util; // Re-export transport traits for generated code pub use crate::transport::server::{GrpcService, NamedService}; @@ -20,8 +26,5 @@ pub use crate::transport::server::{GrpcService, NamedService}; // Re-export server types pub use crate::server::Grpc; -// Monoio doesn't require Send, but futures still need 'static -pub type BoxFuture = self::Pin> + 'static>>; - -// Streams need arena lifetime, not Send -// The 'arena lifetime will be added by generated code where needed +// Monoio doesn't require Send, and futures may borrow from arena +pub type BoxFuture<'a, T, E> = self::Pin> + 'a>>; diff --git a/tocin/src/lib.rs b/tocin/src/lib.rs index 6ecea00..6a78713 100644 --- a/tocin/src/lib.rs +++ b/tocin/src/lib.rs @@ -126,7 +126,8 @@ pub use request::{IntoRequest, IntoStreamingRequest, Request}; pub use response::Response; pub use status::{Code, ConnectError, Status, TimeoutExpired}; -pub(crate) type BoxError = Box; +// Note: BoxError is !Send + !Sync for monoio's thread-per-core model +pub(crate) type BoxError = Box; #[doc(hidden)] #[cfg(feature = "codegen")] diff --git a/tocin/src/server/grpc.rs b/tocin/src/server/grpc.rs index 7fc960a..604ec9b 100644 --- a/tocin/src/server/grpc.rs +++ b/tocin/src/server/grpc.rs @@ -221,7 +221,7 @@ where mut respond: monoio_http::h2::server::SendResponse, ) -> Result<(), Status> where - S: UnaryService<>::Item, Response = T::Encode>, + S: UnaryService<'arena, >::Item, Response = T::Encode>, { let accept_encoding = CompressionEncoding::from_accept_encoding_header( req.headers(), @@ -236,7 +236,7 @@ where }; let response = service - .call(request) + .call(self.arena, request) .await .map(|r| r.map(|m| futures_util::stream::once(futures_util::future::ready(Ok(m))))); @@ -274,7 +274,7 @@ where } }; - let response = service.call(request).await; + let response = service.call(self.arena, request).await; self.send_response( respond, @@ -311,7 +311,7 @@ where }; let response = service - .call(request) + .call(self.arena, request) .await .map(|r| r.map(|m| futures_util::stream::once(futures_util::future::ready(Ok(m))))); @@ -349,7 +349,7 @@ where } }; - let response = service.call(request).await; + let response = service.call(self.arena, request).await; self.send_response( respond, diff --git a/tocin/src/server/service.rs b/tocin/src/server/service.rs index fd7a2d6..369053a 100644 --- a/tocin/src/server/service.rs +++ b/tocin/src/server/service.rs @@ -3,15 +3,18 @@ use std::future::Future; use futures_core::Stream; /// Service for handling unary gRPC requests. -pub trait UnaryService { +/// +/// The lifetime 'arena represents the lifetime of the arena which both +/// request and response messages may borrow from. +pub trait UnaryService<'arena, R> { /// Protobuf response message type type Response; - /// Response future - type Future: Future, Status>>; + /// Response future (may borrow from arena) + type Future: Future, Status>> + 'arena; - /// Call the service - fn call(&mut self, request: Request) -> Self::Future; + /// Call the service with access to the arena + fn call(&mut self, arena: &'arena defiant::Arena, request: Request) -> Self::Future; } /// Service for handling server streaming gRPC requests. @@ -25,11 +28,11 @@ pub trait ServerStreamingService<'arena, R> { /// Stream of outbound response messages (may borrow from arena) type ResponseStream: Stream> + 'arena; - /// Response future - type Future: Future, Status>>; + /// Response future (may borrow from arena) + type Future: Future, Status>> + 'arena; - /// Call the service - fn call(&mut self, request: Request) -> Self::Future; + /// Call the service with access to the arena + fn call(&mut self, arena: &'arena defiant::Arena, request: Request) -> Self::Future; } /// Service for handling client streaming gRPC requests. @@ -40,11 +43,11 @@ pub trait ClientStreamingService<'arena, D> { /// Protobuf response message type type Response; - /// Response future - type Future: Future, Status>>; + /// Response future (may borrow from arena) + type Future: Future, Status>> + 'arena; - /// Call the service - fn call(&mut self, request: Request>) -> Self::Future; + /// Call the service with access to the arena + fn call(&mut self, arena: &'arena defiant::Arena, request: Request>) -> Self::Future; } /// Service for handling bidirectional streaming gRPC requests. @@ -58,9 +61,9 @@ pub trait StreamingService<'arena, D> { /// Stream of outbound response messages (may borrow from arena) type ResponseStream: Stream> + 'arena; - /// Response future - type Future: Future, Status>>; + /// Response future (may borrow from arena) + type Future: Future, Status>> + 'arena; - /// Call the service - fn call(&mut self, request: Request>) -> Self::Future; + /// Call the service with access to the arena + fn call(&mut self, arena: &'arena defiant::Arena, request: Request>) -> Self::Future; } diff --git a/tocin/src/status.rs b/tocin/src/status.rs index 68ca848..fda3d49 100644 --- a/tocin/src/status.rs +++ b/tocin/src/status.rs @@ -51,7 +51,7 @@ struct StatusInner { /// or by `Status` fields above, they will be ignored. metadata: MetadataMap, /// Optional underlying error. - source: Option>, + source: Option>, } impl StatusInner { @@ -317,7 +317,7 @@ impl Status { } pub(crate) fn from_error_generic( - err: impl Into>, + err: impl Into>, ) -> Status { Self::from_error(err.into()) } @@ -326,7 +326,7 @@ impl Status { /// /// Inspects the error source chain for recognizable errors, including statuses, HTTP2, and /// hyper, and attempts to maps them to a `Status`, or else returns an Unknown `Status`. - pub fn from_error(err: Box) -> Status { + pub fn from_error(err: Box) -> Status { Status::try_from_error(err).unwrap_or_else(|err| { let mut status = Status::new(Code::Unknown, err.to_string()); status.0.source = Some(err.into()); @@ -342,8 +342,8 @@ impl Status { /// This function does not provide any stability guarantees around how it will downcast errors into /// status codes. pub fn try_from_error( - err: Box, - ) -> Result> { + err: Box, + ) -> Result> { let err = match err.downcast::() { Ok(status) => { return Ok(*status); @@ -423,9 +423,9 @@ impl Status { #[allow(dead_code)] pub(crate) fn map_error(err: E) -> Status where - E: Into>, + E: Into>, { - let err: Box = err.into(); + let err: Box = err.into(); Status::from_error(err) } @@ -502,7 +502,8 @@ impl Status { &mut self.0.metadata } - pub(crate) fn to_header_map(&self) -> Result { + #[doc(hidden)] + pub fn to_header_map(&self) -> Result { let mut header_map = HeaderMap::with_capacity(3 + self.0.metadata.len()); self.add_header(&mut header_map)?; Ok(header_map) @@ -565,7 +566,7 @@ impl Status { } /// Add a source error to this status. - pub fn set_source(&mut self, source: Arc) -> &mut Status { + pub fn set_source(&mut self, source: Arc) -> &mut Status { self.0.source = Some(source); self } @@ -577,7 +578,9 @@ impl Status { .headers_mut() .insert(http::header::CONTENT_TYPE, GRPC_CONTENT_TYPE); self.add_header(response.headers_mut()).unwrap(); - response.extensions_mut().insert(self); + // Note: We don't insert Status into extensions in tocin because Status is !Send + !Sync + // for monoio's thread-per-core model, but http::Extensions requires Send + Sync + // response.extensions_mut().insert(self); response } @@ -1036,7 +1039,7 @@ impl std::error::Error for TimeoutExpired {} /// Wrapper type to indicate that an error occurs during the connection /// process, so that the appropriate gRPC Status can be inferred. #[derive(Debug)] -pub struct ConnectError(pub Box); +pub struct ConnectError(pub Box); impl fmt::Display for ConnectError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { diff --git a/tocin/src/transport/channel.rs b/tocin/src/transport/channel.rs new file mode 100644 index 0000000..bbe9fb4 --- /dev/null +++ b/tocin/src/transport/channel.rs @@ -0,0 +1,210 @@ +use crate::transport::Error; +use bytes::Bytes; +use http::Uri; +use monoio_http::h2; +use std::cell::RefCell; +use std::rc::Rc; +use std::{fmt, str::FromStr, task::{Context, Poll}}; + +/// HTTP/2 channel for gRPC client connections. +/// +/// This provides a client connection over monoio's h2 implementation. +/// The channel establishes a TCP connection, performs the HTTP/2 handshake, +/// and multiplexes gRPC requests over the single connection. +/// +/// # Example +/// +/// ```rust,no_run +/// use tocin::transport::Channel; +/// +/// # async fn example() -> Result<(), Box> { +/// let channel = Channel::builder("http://[::1]:50051".parse().unwrap()) +/// .connect() +/// .await?; +/// # Ok(()) +/// # } +/// ``` +#[derive(Clone)] +pub struct Channel { + uri: Uri, + sender: Rc>>, +} + +impl Channel { + /// Create an endpoint builder from a static URI string. + pub fn from_static(s: &'static str) -> Endpoint { + let uri = Uri::from_str(s).expect("invalid URI"); + Endpoint::from(uri) + } + + /// Create an endpoint builder from a shared URI string. + pub fn from_shared(s: impl Into>) -> Result { + let s = s.into(); + let uri = Uri::from_maybe_shared(Bytes::from(s)) + .map_err(|_| Error::new_invalid_uri())?; + Ok(Endpoint::from(uri)) + } + + /// Create an endpoint builder from a URI. + pub fn builder(uri: Uri) -> Endpoint { + Endpoint::from(uri) + } + + /// Get the URI this channel is connected to. + pub fn uri(&self) -> &Uri { + &self.uri + } + + /// Connect to the given URI, establishing an HTTP/2 connection. + /// + /// This performs TCP connect → HTTP/2 handshake → spawns the connection + /// driver task. The returned Channel can then be used to send gRPC requests. + pub async fn connect(uri: Uri) -> Result { + // Extract host and port from the URI + let authority = uri.authority() + .ok_or_else(|| Error::new_connect("URI has no authority (host:port)"))?; + + let host = authority.host(); + let port = authority.port_u16().unwrap_or(match uri.scheme_str() { + Some("https") => 443, + _ => 80, + }); + + let addr = format!("{}:{}", host, port); + + tracing::debug!("Connecting to {}", addr); + + // Establish TCP connection using monoio + let tcp = monoio::net::TcpStream::connect(&addr).await + .map_err(|e| Error::new_connect(format!("TCP connect to {} failed: {}", addr, e)))?; + + tracing::debug!("TCP connected, performing HTTP/2 handshake"); + + // Perform HTTP/2 client handshake + let (sender, connection) = h2::client::handshake(tcp).await + .map_err(|e| Error::new_connect(format!("HTTP/2 handshake failed: {}", e)))?; + + // Spawn the connection driver task. + // This must run in the background to process h2 frames. + // In monoio, spawned tasks are !Send which is fine. + monoio::spawn(async move { + if let Err(e) = connection.await { + tracing::error!("HTTP/2 connection error: {:?}", e); + } + }); + + tracing::debug!("HTTP/2 connection established"); + + Ok(Channel { + uri, + sender: Rc::new(RefCell::new(sender)), + }) + } +} + +/// Channel endpoint configuration. +pub struct Endpoint { + uri: Uri, +} + +impl Endpoint { + /// Create a new endpoint. + pub fn new(dst: D) -> Result + where + D: TryInto, + D::Error: Into, + { + dst.try_into().map_err(|e| Error::from_source(e.into())) + } + + /// Connect to the endpoint, creating a Channel. + pub async fn connect(self) -> Result { + Channel::connect(self.uri).await + } +} + +impl From for Endpoint { + fn from(uri: Uri) -> Self { + Endpoint { uri } + } +} + +impl TryFrom<&'static str> for Endpoint { + type Error = Error; + + fn try_from(s: &'static str) -> Result { + let uri = Uri::from_str(s).map_err(|_| Error::new_invalid_uri())?; + Ok(Endpoint::from(uri)) + } +} + +impl fmt::Debug for Channel { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Channel") + .field("uri", &self.uri) + .finish() + } +} + +impl fmt::Debug for Endpoint { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Endpoint") + .field("uri", &self.uri) + .finish() + } +} + +// Implement GrpcService for Channel. +// +// The Channel receives BoxBody (type-erased stream of bytes) as the request body. +// It drains the body into h2 SendStream data frames. +impl crate::client::GrpcService for Channel { + type ResponseBody = h2::RecvStream; + type Error = Error; + + fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { + self.sender.borrow_mut().poll_ready(cx) + .map_err(|e| Error::new_connect(format!("h2 not ready: {}", e))) + } + + async fn call<'a>( + &'a mut self, + req: http::Request>, + ) -> Result, Error> { + let (parts, body) = req.into_parts(); + + // Build the h2 request with an empty body (headers only). + // We'll send body data separately via SendStream. + let h2_request = http::Request::from_parts(parts, ()); + + // For gRPC, there's always at least the encoded message, + // so we don't end the stream with the headers. + let end_of_stream = false; + + let (response_future, mut send_stream) = { + let mut sender = self.sender.borrow_mut(); + sender.send_request(h2_request, end_of_stream) + .map_err(|e| Error::new_connect(format!("h2 send_request failed: {}", e)))? + }; + + // Stream the body data into the h2 SendStream + use futures_util::StreamExt; + let mut body = std::pin::pin!(body); + while let Some(result) = body.next().await { + let chunk = result + .map_err(|e| Error::new_connect(format!("body stream error: {}", e)))?; + send_stream.send_data(chunk, false) + .map_err(|e| Error::new_connect(format!("h2 send_data failed: {}", e)))?; + } + + // Signal end of stream + send_stream.send_data(Bytes::new(), true) + .map_err(|e| Error::new_connect(format!("h2 send_data (eos) failed: {}", e)))?; + + // Wait for the response + let response = response_future.await + .map_err(|e| Error::new_connect(format!("h2 response failed: {}", e)))?; + + Ok(response) + } +} diff --git a/tocin/src/transport/error.rs b/tocin/src/transport/error.rs new file mode 100644 index 0000000..600c74d --- /dev/null +++ b/tocin/src/transport/error.rs @@ -0,0 +1,84 @@ +use std::{error::Error as StdError, fmt}; + +// Note: Source is !Send + !Sync to work with monoio's thread-per-core model +type Source = Box; + +/// Errors that originate from the client or server transport +pub struct Error { + inner: ErrorImpl, +} + +struct ErrorImpl { + kind: Kind, + source: Option, +} + +#[derive(Debug)] +pub(crate) enum Kind { + Transport, + InvalidUri, + Connect, +} + +impl Error { + pub(crate) fn new(kind: Kind) -> Self { + Self { + inner: ErrorImpl { kind, source: None }, + } + } + + pub(crate) fn with(mut self, source: impl Into) -> Self { + self.inner.source = Some(source.into()); + self + } + + pub(crate) fn from_source(source: impl Into) -> Self { + let boxed: crate::BoxError = source.into(); + Error::new(Kind::Transport).with(boxed) + } + + pub(crate) fn new_invalid_uri() -> Self { + Error::new(Kind::InvalidUri) + } + + pub(crate) fn new_connect>(source: E) -> Self { + Error::new(Kind::Connect).with(source) + } + + fn description(&self) -> &str { + match &self.inner.kind { + Kind::Transport => "transport error", + Kind::InvalidUri => "invalid URI", + Kind::Connect => "connection error", + } + } +} + +impl fmt::Debug for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut f = f.debug_tuple("tocin::transport::Error"); + + f.field(&self.inner.kind); + + if let Some(source) = &self.inner.source { + f.field(source); + } + + f.finish() + } +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(self.description()) + } +} + +impl StdError for Error { + fn source(&self) -> Option<&(dyn StdError + 'static)> { + self.inner + .source + .as_ref() + .map(|source| &**source as &(dyn StdError + 'static)) + } +} diff --git a/tocin/src/transport/mod.rs b/tocin/src/transport/mod.rs index 6e113f3..590dd8e 100644 --- a/tocin/src/transport/mod.rs +++ b/tocin/src/transport/mod.rs @@ -4,5 +4,9 @@ //! async runtime and monoio-http's HTTP/2 stack. pub mod server; +pub mod channel; +mod error; pub use server::Server; +pub use channel::{Channel, Endpoint}; +pub use error::Error; diff --git a/tocin/src/transport/server.rs b/tocin/src/transport/server.rs index 6cfb278..7399ae6 100644 --- a/tocin/src/transport/server.rs +++ b/tocin/src/transport/server.rs @@ -23,16 +23,21 @@ pub trait NamedService { /// This is the main trait that routes requests to service implementations. /// The generated code will implement this for each service. pub trait GrpcService { + /// The future returned by call + type CallFuture<'arena>: std::future::Future> + 'arena + where + Self: 'arena; + /// Handle a gRPC request. /// /// The service receives the arena, request, and response sender, and is /// responsible for calling the appropriate handler method. - async fn call( - &mut self, - arena: &defiant::Arena, + fn call<'arena>( + &'arena mut self, + arena: &'arena defiant::Arena, request: http::Request, respond: h2::server::SendResponse, - ) -> Result<(), crate::Status>; + ) -> Self::CallFuture<'arena>; } /// Errors that can occur in the transport layer.