|
| 1 | +# Rust Implementation Plan: Request Header Code Generation |
| 2 | + |
| 3 | +## Overview |
| 4 | + |
| 5 | +This plan outlines the implementation of a Rust equivalent to the Go code generator added in [temporalio/api-go#236](https://github.com/temporalio/api-go/pull/236) that automatically extracts field values from request messages and propagates them as HTTP headers based on protobuf annotations defined in [temporalio/api#728](https://github.com/temporalio/api/pull/728). |
| 6 | + |
| 7 | +## Background |
| 8 | + |
| 9 | +The Go implementation adds: |
| 10 | +1. Proto annotations (`temporal.api.protometa.v1.request_header`) that specify which fields should be extracted from request messages and set as headers |
| 11 | +2. A code generator that parses these annotations and generates header extraction logic |
| 12 | +3. Runtime functions to extract headers from request messages and add them to outgoing gRPC metadata |
| 13 | + |
| 14 | +The annotations support template interpolation (e.g., `"workflow:{workflow_id}"`) where field paths in braces are replaced with actual field values. |
| 15 | + |
| 16 | +## Current State Analysis |
| 17 | + |
| 18 | +### Existing Rust Infrastructure |
| 19 | +- **Protobuf Generation**: Uses `tonic_prost_build` in `crates/common/build.rs` to generate Rust types from proto files |
| 20 | +- **Descriptor Access**: Already generates and saves `descriptors.bin` for use by existing code generators |
| 21 | +- **Code Generation Pattern**: Existing `PayloadVisitorGenerator` shows the pattern for parsing descriptors and generating Rust code |
| 22 | +- **gRPC Client**: `crates/client/src/grpc.rs` handles gRPC service calls with existing header support |
| 23 | +- **Request Extensions**: `crates/client/src/request_extensions.rs` provides mechanisms for request modification |
| 24 | + |
| 25 | +### Proto Annotations Available |
| 26 | +The `temporal.api.protometa.v1.annotations.proto` file defines: |
| 27 | +- `RequestHeaderAnnotation` message with `header` and `value` fields |
| 28 | +- `request_header` extension for `google.protobuf.MethodOptions` |
| 29 | +- Support for template interpolation with field paths in braces |
| 30 | + |
| 31 | +## Implementation Plan |
| 32 | + |
| 33 | +### Phase 1: Core Generator Infrastructure |
| 34 | + |
| 35 | +#### 1.1 Request Header Generator Module |
| 36 | +**Location**: `crates/common/build.rs` (extend existing build script) |
| 37 | + |
| 38 | +**Components**: |
| 39 | +- `RequestHeaderGenerator` struct to parse proto descriptors and extract annotation information |
| 40 | +- Logic to identify methods with `request_header` annotations |
| 41 | +- Template parsing to extract field paths from value templates (e.g., `"{workflow_execution.workflow_id}"`) |
| 42 | +- Field accessor generation for nested proto field navigation |
| 43 | + |
| 44 | +**Key Functions**: |
| 45 | +```rust |
| 46 | +struct RequestHeaderGenerator { |
| 47 | + // Maps method full names to their header extraction info |
| 48 | + method_headers: HashMap<String, Vec<MethodHeaderInfo>>, |
| 49 | +} |
| 50 | + |
| 51 | +struct MethodHeaderInfo { |
| 52 | + service_name: String, |
| 53 | + method_name: String, |
| 54 | + request_type: String, |
| 55 | + headers: Vec<HeaderInfo>, |
| 56 | +} |
| 57 | + |
| 58 | +struct HeaderInfo { |
| 59 | + header_name: String, |
| 60 | + value_template: String, |
| 61 | + field_paths: Vec<String>, // Extracted from template |
| 62 | +} |
| 63 | +``` |
| 64 | + |
| 65 | +#### 1.2 Code Generation Templates |
| 66 | +Generate Rust code similar to Go's approach but idiomatic to Rust: |
| 67 | + |
| 68 | +```rust |
| 69 | +// Generated function signature |
| 70 | +pub fn extract_temporal_request_headers<T>( |
| 71 | + request: &T, |
| 72 | + existing_metadata: Option<&tonic::metadata::MetadataMap>, |
| 73 | +) -> Vec<(String, String)> |
| 74 | +where |
| 75 | + T: std::any::Any, |
| 76 | +{ |
| 77 | + // Type-based dispatch and header extraction |
| 78 | +} |
| 79 | +``` |
| 80 | + |
| 81 | +#### 1.3 Integration with Build Process |
| 82 | +Extend `crates/common/build.rs` to: |
| 83 | +- Call the header generator after proto compilation |
| 84 | +- Generate `request_header_impl.rs` alongside existing generated files |
| 85 | +- Include the generated file in the build output |
| 86 | + |
| 87 | +### Phase 2: Runtime Header Extraction |
| 88 | + |
| 89 | +#### 2.1 Header Extraction API |
| 90 | +**Location**: `crates/client/src/request_headers.rs` (new module) |
| 91 | + |
| 92 | +**Core API**: |
| 93 | +```rust |
| 94 | +pub struct HeaderExtractionOptions<'a> { |
| 95 | + pub existing_metadata: Option<&'a tonic::metadata::MetadataMap>, |
| 96 | + pub include_namespace: bool, // Whether to add temporal-namespace header |
| 97 | +} |
| 98 | + |
| 99 | +pub fn extract_request_headers<T>( |
| 100 | + request: &T, |
| 101 | + opts: HeaderExtractionOptions<'_>, |
| 102 | +) -> Vec<(String, String)> |
| 103 | +where |
| 104 | + T: std::any::Any + Send + Sync, |
| 105 | +``` |
| 106 | + |
| 107 | +#### 2.2 Field Accessor Utilities |
| 108 | +Generate helper functions for navigating proto field paths: |
| 109 | +```rust |
| 110 | +// Example generated accessor for "{workflow_execution.workflow_id}" |
| 111 | +fn get_workflow_execution_workflow_id(request: &StartWorkflowExecutionRequest) -> Option<&str> { |
| 112 | + request.workflow_execution.as_ref()?.workflow_id.as_deref() |
| 113 | +} |
| 114 | +``` |
| 115 | + |
| 116 | +#### 2.3 Template Interpolation |
| 117 | +Handle template strings with field path substitution: |
| 118 | +```rust |
| 119 | +fn interpolate_template(template: &str, field_values: &[(&str, &str)]) -> String { |
| 120 | + // Replace "{field_path}" with actual values |
| 121 | +} |
| 122 | +``` |
| 123 | + |
| 124 | +### Phase 3: gRPC Client Integration |
| 125 | + |
| 126 | +#### 3.1 Request Interceptor |
| 127 | +**Location**: `crates/client/src/grpc.rs` (extend existing) |
| 128 | + |
| 129 | +Add header extraction to the gRPC call pipeline: |
| 130 | +```rust |
| 131 | +impl<T> RawGrpcCaller for HeaderExtractingClient<T> |
| 132 | +where |
| 133 | + T: RawGrpcCaller, |
| 134 | +{ |
| 135 | + async fn call<F, Req, Resp>( |
| 136 | + &mut self, |
| 137 | + call_name: &'static str, |
| 138 | + mut callfn: F, |
| 139 | + mut req: Request<Req>, |
| 140 | + ) -> Result<Response<Resp>, Status> |
| 141 | + where |
| 142 | + // ... trait bounds |
| 143 | + { |
| 144 | + // Extract headers from request body |
| 145 | + let headers = extract_request_headers( |
| 146 | + req.get_ref(), |
| 147 | + HeaderExtractionOptions { |
| 148 | + existing_metadata: Some(req.metadata()), |
| 149 | + include_namespace: true, |
| 150 | + }, |
| 151 | + ); |
| 152 | + |
| 153 | + // Add headers to request metadata |
| 154 | + for (key, value) in headers { |
| 155 | + if !req.metadata().contains_key(&key) { |
| 156 | + req.metadata_mut().insert( |
| 157 | + key.try_into()?, |
| 158 | + value.try_into()?, |
| 159 | + ); |
| 160 | + } |
| 161 | + } |
| 162 | + |
| 163 | + self.inner.call(call_name, callfn, req).await |
| 164 | + } |
| 165 | +} |
| 166 | +``` |
| 167 | + |
| 168 | +### Phase 4: Error Handling & Edge Cases |
| 169 | + |
| 170 | +#### 4.1 Error Handling Strategy |
| 171 | +- **Build-time errors**: Invalid annotations, missing fields, type mismatches |
| 172 | +- **Runtime errors**: Invalid header values, metadata insertion failures |
| 173 | +- **Graceful degradation**: Continue operation if header extraction fails |
| 174 | + |
| 175 | +#### 4.2 Edge Cases |
| 176 | +- **Empty/missing fields**: Skip header if field value is empty |
| 177 | +- **Nested message navigation**: Handle Option<> types in field chains |
| 178 | +- **Duplicate headers**: Respect existing metadata, don't override |
| 179 | +- **Invalid template syntax**: Compile-time validation of template strings |
| 180 | + |
| 181 | +### Phase 5: Testing & Validation |
| 182 | + |
| 183 | +#### 5.1 Unit Tests |
| 184 | +- Template parsing and field path extraction |
| 185 | +- Header generation for various request types |
| 186 | +- Edge cases and error conditions |
| 187 | + |
| 188 | +#### 5.2 Integration Tests |
| 189 | +- End-to-end header propagation in gRPC calls |
| 190 | +- Compatibility with existing client functionality |
| 191 | +- Performance impact measurement |
| 192 | + |
| 193 | +#### 5.3 Compatibility Testing |
| 194 | +- Verify generated headers match Go implementation |
| 195 | +- Test with actual Temporal server routing |
| 196 | + |
| 197 | +## Implementation Questions & Considerations |
| 198 | + |
| 199 | +### 1. Code Generation Approach |
| 200 | +**Question**: Should we generate a single function that handles all request types, or individual functions per request type? |
| 201 | + |
| 202 | +**Recommendation**: Single function with type-based dispatch (using `std::any::Any`) for better maintainability and smaller generated code size. |
| 203 | + |
| 204 | +### 2. Performance Considerations |
| 205 | +**Question**: What's the performance impact of reflection-based type dispatch? |
| 206 | + |
| 207 | +**Considerations**: |
| 208 | +- Use compile-time type information where possible |
| 209 | +- Consider caching field accessors |
| 210 | +- Benchmark against Go implementation |
| 211 | + |
| 212 | +### 3. Integration Point |
| 213 | +**Question**: Where should header extraction be integrated in the client pipeline? |
| 214 | + |
| 215 | +**Recommendation**: In `RawGrpcCaller::call` to ensure all requests go through header extraction automatically. |
| 216 | + |
| 217 | +### 4. Namespace Header Handling |
| 218 | +**Question**: Should we always include the namespace header like the Go implementation? |
| 219 | + |
| 220 | +**Recommendation**: Yes, maintain compatibility by always checking for namespace field and adding the header if present. |
| 221 | + |
| 222 | +### 5. Template Syntax Validation |
| 223 | +**Question**: Should we validate template syntax at compile time or runtime? |
| 224 | + |
| 225 | +**Recommendation**: Compile-time validation during code generation to catch errors early. |
| 226 | + |
| 227 | +## Dependencies |
| 228 | + |
| 229 | +- No new external dependencies required |
| 230 | +- Uses existing `prost-types`, `tonic`, and `std::any` functionality |
| 231 | +- Builds on existing code generation patterns in the codebase |
| 232 | + |
| 233 | +## Future Considerations |
| 234 | + |
| 235 | +1. **Performance optimization**: Consider compile-time code generation for better performance |
| 236 | +2. **Additional header types**: Support for other header patterns beyond resource-id |
| 237 | +3. **Dynamic header configuration**: Runtime configuration of header extraction rules |
| 238 | +4. **Metrics**: Add telemetry for header extraction success/failure rates |
| 239 | + |
| 240 | +## Conclusion |
| 241 | + |
| 242 | +This implementation will provide Rust SDK users with automatic request header propagation equivalent to the Go SDK, enabling proper request routing in multi-cluster Temporal deployments while maintaining the existing client API and performance characteristics. |
0 commit comments