tonic-rails 0.5.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,665 @@
1
+ //! `TonicRails::Native::Channel`: outgoing gRPC calls over a tonic
2
+ //! [`Channel`], with message bytes passed through untouched. Unary calls
3
+ //! return their result; server-streaming calls return a
4
+ //! `TonicRails::Native::ResponseStream` to read messages from.
5
+ //!
6
+ //! Fork safety: each process gets its own tokio runtime and its own
7
+ //! connection, created on first use and keyed by pid. After `fork` the
8
+ //! child sees a different pid and builds fresh ones. It never touches
9
+ //! what it inherited: the runtime's threads don't exist in the child, so
10
+ //! dropping the runtime (or a channel whose tasks ran on it) could hang
11
+ //! or wake the parent's event loop. Those are leaked instead.
12
+ //!
13
+ //! The system's root certificates are loaded once and kept, so children
14
+ //! inherit them. On macOS loading them goes through Security.framework,
15
+ //! which aborts in the child of a multi-threaded process.
16
+
17
+ use std::future::Future;
18
+ use std::io;
19
+ use std::sync::{Arc, OnceLock};
20
+ use std::time::Duration;
21
+
22
+ use base64::Engine;
23
+ use bytes::{Buf, BufMut, Bytes};
24
+ use http::uri::PathAndQuery;
25
+ use http::{HeaderMap, HeaderName, HeaderValue};
26
+ use magnus::{Error, RArray, RHash, RString, Ruby, r_hash::ForEach};
27
+ use parking_lot::Mutex;
28
+ use tokio::runtime::{Builder, Handle, Runtime};
29
+ use tokio::sync::mpsc;
30
+ use tokio::task::JoinHandle;
31
+ use tonic::codec::{Codec, DecodeBuf, Decoder, EncodeBuf, Encoder, Streaming};
32
+ use tonic::metadata::MetadataMap;
33
+ use tonic::transport::{Certificate, Channel, ClientTlsConfig, Endpoint, Identity};
34
+ use tonic::{Code, Request, Status};
35
+
36
+ use crate::gvl::{Cancel, Wait, block_on, recv_blocking, without_gvl};
37
+
38
+ // ─── Per-process runtime ──────────────────────────────────────────
39
+
40
+ struct ProcessRuntime {
41
+ pid: u32,
42
+ runtime: Runtime,
43
+ }
44
+
45
+ static RUNTIME: Mutex<Option<ProcessRuntime>> = Mutex::new(None);
46
+
47
+ /// I/O for outgoing calls is light: two threads serve every client in
48
+ /// the process.
49
+ const RUNTIME_THREADS: usize = 2;
50
+
51
+ /// The current process's runtime, created on first use.
52
+ fn runtime() -> io::Result<Handle> {
53
+ let pid = std::process::id();
54
+ if let Some(current) = RUNTIME.lock().as_ref().filter(|r| r.pid == pid) {
55
+ return Ok(current.runtime.handle().clone());
56
+ }
57
+ // Built outside the lock: if another thread forked while this one
58
+ // held it, the child's copy of the lock would never be released.
59
+ let runtime = Builder::new_multi_thread()
60
+ .worker_threads(RUNTIME_THREADS)
61
+ .thread_name("tonic-rails-client")
62
+ .enable_all()
63
+ .build()?;
64
+ let mut slot = RUNTIME.lock();
65
+ if let Some(current) = slot.as_ref().filter(|r| r.pid == pid) {
66
+ // Another thread got there first.
67
+ runtime.shutdown_background();
68
+ return Ok(current.runtime.handle().clone());
69
+ }
70
+ let handle = runtime.handle().clone();
71
+ if let Some(inherited) = slot.replace(ProcessRuntime { pid, runtime }) {
72
+ std::mem::forget(inherited);
73
+ }
74
+ Ok(handle)
75
+ }
76
+
77
+ // ─── Raw-bytes codec ──────────────────────────────────────────────
78
+
79
+ /// Passes encoded protobuf messages through: Ruby encodes requests and
80
+ /// decodes responses.
81
+ #[derive(Clone, Copy, Default)]
82
+ struct RawCodec;
83
+
84
+ impl Codec for RawCodec {
85
+ type Encode = Bytes;
86
+ type Decode = Bytes;
87
+ type Encoder = RawCodec;
88
+ type Decoder = RawCodec;
89
+
90
+ fn encoder(&mut self) -> Self::Encoder {
91
+ *self
92
+ }
93
+
94
+ fn decoder(&mut self) -> Self::Decoder {
95
+ *self
96
+ }
97
+ }
98
+
99
+ impl Encoder for RawCodec {
100
+ type Item = Bytes;
101
+ type Error = Status;
102
+
103
+ fn encode(&mut self, item: Bytes, dst: &mut EncodeBuf<'_>) -> Result<(), Status> {
104
+ dst.put(item);
105
+ Ok(())
106
+ }
107
+ }
108
+
109
+ impl Decoder for RawCodec {
110
+ type Item = Bytes;
111
+ type Error = Status;
112
+
113
+ fn decode(&mut self, src: &mut DecodeBuf<'_>) -> Result<Option<Bytes>, Status> {
114
+ Ok(Some(src.copy_to_bytes(src.remaining())))
115
+ }
116
+ }
117
+
118
+ // ─── Calls ────────────────────────────────────────────────────────
119
+
120
+ /// How a call ended. `code` 0 means OK and `response` is set.
121
+ struct Outcome {
122
+ code: Code,
123
+ message: String,
124
+ /// The failure happened here (a broken connection, a timeout), not
125
+ /// on the server.
126
+ local: bool,
127
+ /// Response headers arrived, so the server has started on the call
128
+ /// and retrying it could repeat its work.
129
+ committed: bool,
130
+ details: Bytes,
131
+ response: Option<Bytes>,
132
+ headers: HeaderMap,
133
+ trailers: HeaderMap,
134
+ }
135
+
136
+ impl Outcome {
137
+ fn ok(response: Option<Bytes>, headers: HeaderMap, trailers: HeaderMap) -> Self {
138
+ Self {
139
+ code: Code::Ok,
140
+ message: String::new(),
141
+ local: false,
142
+ committed: true,
143
+ details: Bytes::new(),
144
+ response,
145
+ headers,
146
+ trailers,
147
+ }
148
+ }
149
+
150
+ fn error(code: Code, message: impl Into<String>) -> Self {
151
+ Self {
152
+ local: true,
153
+ ..Self::status(Status::new(code, message), None)
154
+ }
155
+ }
156
+
157
+ /// `headers` are the response headers, if they arrived before the
158
+ /// status.
159
+ fn status(status: Status, headers: Option<HeaderMap>) -> Self {
160
+ // A status with a source is a local transport failure, not one the
161
+ // server sent. The source holds the cause (e.g. "Connection
162
+ // refused"), and tonic reports broken connections as UNKNOWN or
163
+ // CANCELLED where gRPC clients report UNAVAILABLE.
164
+ let (code, message, local) = match std::error::Error::source(&status) {
165
+ Some(source) => (
166
+ match status.code() {
167
+ Code::Unknown | Code::Cancelled => Code::Unavailable,
168
+ code => code,
169
+ },
170
+ append_causes(status.message().to_owned(), Some(source)),
171
+ true,
172
+ ),
173
+ None => (status.code(), status.message().to_owned(), false),
174
+ };
175
+ Self {
176
+ code,
177
+ message,
178
+ local,
179
+ committed: headers.is_some(),
180
+ details: Bytes::copy_from_slice(status.details()),
181
+ response: None,
182
+ headers: headers.unwrap_or_default(),
183
+ trailers: status.metadata().clone().into_headers(),
184
+ }
185
+ }
186
+
187
+ /// `[code, message, response, headers, trailers, details, committed]`,
188
+ /// with metadata as `[name, value]` pairs in wire form (`-bin` values
189
+ /// base64-encoded).
190
+ fn into_ruby(self, ruby: &Ruby) -> Result<RArray, Error> {
191
+ let result = ruby.ary_new_capa(7);
192
+ result.push(self.code as u32)?;
193
+ result.push(ruby.str_new(&self.message))?;
194
+ result.push(self.response.map(|bytes| ruby.str_from_slice(&bytes)))?;
195
+ result.push(metadata_pairs(ruby, &self.headers)?)?;
196
+ result.push(metadata_pairs(ruby, &self.trailers)?)?;
197
+ result.push((!self.details.is_empty()).then(|| ruby.str_from_slice(&self.details)))?;
198
+ result.push(self.committed)?;
199
+ Ok(result)
200
+ }
201
+ }
202
+
203
+ fn metadata_pairs(ruby: &Ruby, headers: &HeaderMap) -> Result<RArray, Error> {
204
+ let pairs = ruby.ary_new_capa(headers.len());
205
+ for (name, value) in headers {
206
+ let value = String::from_utf8_lossy(value.as_bytes());
207
+ pairs.push((ruby.str_new(name.as_str()), ruby.str_new(&value)))?;
208
+ }
209
+ Ok(pairs)
210
+ }
211
+
212
+ /// Bound a call by `timeout`, if there is one. For a stream, that
213
+ /// covers every message, including time spent waiting on a slow reader.
214
+ async fn with_timeout(call: impl Future<Output = Outcome>, timeout: Option<Duration>) -> Outcome {
215
+ let Some(timeout) = timeout else {
216
+ return call.await;
217
+ };
218
+ let started = tokio::time::Instant::now();
219
+ let outcome = tokio::time::timeout(timeout, call).await;
220
+ match outcome {
221
+ // tonic's channel enforces the grpc-timeout header too, but ends
222
+ // the call with its own status.
223
+ Ok(outcome) if !(outcome.local && started.elapsed() >= timeout) => outcome,
224
+ _ => Outcome::error(Code::DeadlineExceeded, "deadline exceeded"),
225
+ }
226
+ }
227
+
228
+ /// What every call needs: the method, one request message and its metadata.
229
+ struct CallArgs {
230
+ channel: Channel,
231
+ path: PathAndQuery,
232
+ request: Bytes,
233
+ metadata: HeaderMap,
234
+ timeout: Option<Duration>,
235
+ max_message_size: usize,
236
+ }
237
+
238
+ /// Send the request and wait for the response headers.
239
+ async fn start(args: CallArgs) -> Result<(HeaderMap, Streaming<Bytes>), Box<Outcome>> {
240
+ let mut grpc = tonic::client::Grpc::new(args.channel)
241
+ .max_decoding_message_size(args.max_message_size)
242
+ .max_encoding_message_size(args.max_message_size);
243
+ if let Err(e) = grpc.ready().await {
244
+ let message = format!("channel not ready: {e}");
245
+ return Err(Box::new(Outcome::error(Code::Unavailable, message)));
246
+ }
247
+
248
+ // The streaming call keeps response headers and trailers apart, which
249
+ // tonic's own unary call merges.
250
+ let mut request = Request::from_parts(
251
+ MetadataMap::from_headers(args.metadata),
252
+ Default::default(),
253
+ tokio_stream::once(args.request),
254
+ );
255
+ if let Some(timeout) = args.timeout {
256
+ request.set_timeout(timeout);
257
+ }
258
+ match grpc.streaming(request, args.path, RawCodec).await {
259
+ Ok(response) => {
260
+ let (headers, stream, _) = response.into_parts();
261
+ Ok((headers.into_headers(), stream))
262
+ }
263
+ // Includes trailers-only responses, whose status comes in the
264
+ // headers. They don't commit the call: gRPC retries treat a server
265
+ // that answers with only a status as not having started on it.
266
+ Err(status) => Err(Box::new(Outcome::status(status, None))),
267
+ }
268
+ }
269
+
270
+ /// The trailers that end a successful call.
271
+ async fn finish(stream: &mut Streaming<Bytes>, headers: &HeaderMap) -> Result<HeaderMap, Box<Outcome>> {
272
+ match stream.trailers().await {
273
+ Ok(trailers) => Ok(trailers.map(MetadataMap::into_headers).unwrap_or_default()),
274
+ Err(status) => Err(Box::new(Outcome::status(status, Some(headers.clone())))),
275
+ }
276
+ }
277
+
278
+ async fn unary(args: CallArgs) -> Outcome {
279
+ let (headers, mut stream) = match start(args).await {
280
+ Ok(started) => started,
281
+ Err(outcome) => return *outcome,
282
+ };
283
+ let message = match stream.message().await {
284
+ Ok(message) => message,
285
+ Err(status) => return Outcome::status(status, Some(headers)),
286
+ };
287
+ let trailers = match finish(&mut stream, &headers).await {
288
+ Ok(trailers) => trailers,
289
+ Err(outcome) => return *outcome,
290
+ };
291
+ match message {
292
+ Some(message) => Outcome::ok(Some(message), headers, trailers),
293
+ None => Outcome::error(Code::Internal, "server sent no response message"),
294
+ }
295
+ }
296
+
297
+ /// Forward each response message to `tx` as it arrives. A full `tx`
298
+ /// stops reading from the connection, so HTTP/2 flow control holds the
299
+ /// server back.
300
+ async fn server_stream(args: CallArgs, tx: mpsc::Sender<Bytes>) -> Outcome {
301
+ let (headers, mut stream) = match start(args).await {
302
+ Ok(started) => started,
303
+ Err(outcome) => return *outcome,
304
+ };
305
+ loop {
306
+ match stream.message().await {
307
+ Ok(Some(message)) => {
308
+ if tx.send(message).await.is_err() {
309
+ return Outcome::error(Code::Cancelled, "stream closed by the client");
310
+ }
311
+ }
312
+ Ok(None) => break,
313
+ Err(status) => return Outcome::status(status, Some(headers)),
314
+ }
315
+ }
316
+ match finish(&mut stream, &headers).await {
317
+ Ok(trailers) => Outcome::ok(None, headers, trailers),
318
+ Err(outcome) => *outcome,
319
+ }
320
+ }
321
+
322
+ /// Aborts the task if the waiting Ruby thread gives up on it, which
323
+ /// resets the HTTP/2 stream so the server sees the call cancelled.
324
+ struct AbortOnDrop<T>(JoinHandle<T>);
325
+
326
+ impl<T> Drop for AbortOnDrop<T> {
327
+ fn drop(&mut self) {
328
+ self.0.abort();
329
+ }
330
+ }
331
+
332
+ /// Wait for `task` without the GVL. Interrupts (`Thread#raise`,
333
+ /// `Timeout`, signals) are handled by Ruby; if one raises, the task is
334
+ /// aborted.
335
+ fn wait<T: Send + 'static>(ruby: &Ruby, mut task: AbortOnDrop<T>) -> Result<T, Error> {
336
+ loop {
337
+ let cancel = Cancel::default();
338
+ let done = without_gvl(&cancel, || {
339
+ block_on(async {
340
+ tokio::select! {
341
+ biased;
342
+ result = &mut task.0 => Some(result),
343
+ _ = cancel.cancelled() => None,
344
+ }
345
+ })
346
+ });
347
+ match done {
348
+ Some(Some(Ok(value))) => return Ok(value),
349
+ Some(Some(Err(e))) => {
350
+ return Err(Error::new(
351
+ ruby.exception_runtime_error(),
352
+ format!("tonic-rails client call failed: {e}"),
353
+ ));
354
+ }
355
+ _ => ruby.thread_check_ints()?,
356
+ }
357
+ }
358
+ }
359
+
360
+ // ─── Ruby class ───────────────────────────────────────────────────
361
+
362
+ /// `TonicRails::Native::Channel` — one HTTP/2 connection (per process) to
363
+ /// a gRPC server, shared by every thread.
364
+ #[derive(magnus::TypedData)]
365
+ #[magnus(class = "TonicRails::Native::Channel", free_immediately)]
366
+ pub struct NativeChannel {
367
+ endpoint: Endpoint,
368
+ max_message_size: usize,
369
+ /// The connection for the process that created it.
370
+ channel: Mutex<Option<(u32, Channel)>>,
371
+ }
372
+
373
+ impl magnus::typed_data::DataTypeFunctions for NativeChannel {}
374
+
375
+ impl Drop for NativeChannel {
376
+ fn drop(&mut self) {
377
+ if let Some((pid, channel)) = self.channel.get_mut().take()
378
+ && pid != std::process::id()
379
+ {
380
+ // Its tasks ran on the parent's runtime: see the module docs.
381
+ std::mem::forget(channel);
382
+ }
383
+ }
384
+ }
385
+
386
+ /// `[ca_pem, cert_pem, key_pem, server_name]`, each optional.
387
+ type TlsOptions = (Option<String>, Option<String>, Option<String>, Option<String>);
388
+
389
+ impl NativeChannel {
390
+ /// `uri` is `http://host:port` or `https://host:port`. With `tls`
391
+ /// and no CA, the system's root certificates are trusted.
392
+ pub fn new(
393
+ ruby: &Ruby,
394
+ uri: String,
395
+ max_message_size: usize,
396
+ connect_timeout_ms: u64,
397
+ tls: Option<TlsOptions>,
398
+ ) -> Result<Self, Error> {
399
+ let argument_error = |message: String| Error::new(ruby.exception_arg_error(), message);
400
+ let mut endpoint = Endpoint::from_shared(uri.clone())
401
+ .map_err(|e| argument_error(format!("invalid gRPC address {uri}: {e}")))?
402
+ .connect_timeout(Duration::from_millis(connect_timeout_ms))
403
+ .tcp_nodelay(true)
404
+ .user_agent(concat!("tonic-rails/", env!("CARGO_PKG_VERSION")))
405
+ .map_err(|e| argument_error(e.to_string()))?;
406
+ if let Some((ca, cert, key, server_name)) = tls {
407
+ let mut config = ClientTlsConfig::new();
408
+ config = match ca {
409
+ Some(ca) => config.ca_certificate(Certificate::from_pem(ca)),
410
+ None => config.ca_certificate(Certificate::from_pem(system_roots().map_err(argument_error)?)),
411
+ };
412
+ if let (Some(cert), Some(key)) = (cert, key) {
413
+ config = config.identity(Identity::from_pem(cert, key));
414
+ }
415
+ if let Some(server_name) = server_name {
416
+ config = config.domain_name(server_name);
417
+ }
418
+ endpoint = endpoint
419
+ .tls_config(config)
420
+ .map_err(|e| argument_error(format!("invalid TLS configuration: {}", source_chain(&e))))?;
421
+ }
422
+ Ok(Self {
423
+ endpoint,
424
+ max_message_size,
425
+ channel: Mutex::new(None),
426
+ })
427
+ }
428
+
429
+ /// This process's connection, created (lazily connecting) on first use.
430
+ fn channel(&self, runtime: &Handle) -> Channel {
431
+ let pid = std::process::id();
432
+ let mut slot = self.channel.lock();
433
+ if let Some((owner, channel)) = slot.as_ref()
434
+ && *owner == pid
435
+ {
436
+ return channel.clone();
437
+ }
438
+ let _entered = runtime.enter();
439
+ let channel = self.endpoint.connect_lazy();
440
+ if let Some((_, inherited)) = slot.replace((pid, channel.clone())) {
441
+ std::mem::forget(inherited);
442
+ }
443
+ channel
444
+ }
445
+
446
+ /// Everything a call needs, from its Ruby arguments.
447
+ fn call_args(
448
+ ruby: &Ruby,
449
+ this: &Self,
450
+ path: String,
451
+ request: RString,
452
+ metadata: RHash,
453
+ timeout_ms: Option<u64>,
454
+ ) -> Result<(Handle, CallArgs), Error> {
455
+ let path = PathAndQuery::try_from(path).map_err(|e| {
456
+ Error::new(
457
+ ruby.exception_arg_error(),
458
+ format!("invalid gRPC method path: {e}"),
459
+ )
460
+ })?;
461
+ let metadata = header_map(ruby, metadata)?;
462
+ let request = Bytes::copy_from_slice(unsafe { request.as_slice() });
463
+ let runtime = runtime().map_err(|e| {
464
+ Error::new(
465
+ ruby.exception_runtime_error(),
466
+ format!("tonic-rails client: can't start a tokio runtime: {e}"),
467
+ )
468
+ })?;
469
+ let args = CallArgs {
470
+ channel: this.channel(&runtime),
471
+ path,
472
+ request,
473
+ metadata,
474
+ timeout: timeout_ms.map(Duration::from_millis),
475
+ max_message_size: this.max_message_size,
476
+ };
477
+ Ok((runtime, args))
478
+ }
479
+
480
+ /// Make a unary call. Returns the array described in
481
+ /// [`Outcome::into_ruby`]; errors come back as a status, not raised.
482
+ pub fn unary(
483
+ ruby: &Ruby,
484
+ this: &Self,
485
+ path: String,
486
+ request: RString,
487
+ metadata: RHash,
488
+ timeout_ms: Option<u64>,
489
+ ) -> Result<RArray, Error> {
490
+ let (runtime, args) = Self::call_args(ruby, this, path, request, metadata, timeout_ms)?;
491
+ let timeout = args.timeout;
492
+ let task = AbortOnDrop(runtime.spawn(with_timeout(unary(args), timeout)));
493
+ wait(ruby, task)?.into_ruby(ruby)
494
+ }
495
+
496
+ /// Start a server-streaming call. It runs in the background, a few
497
+ /// messages ahead of the reader.
498
+ pub fn server_streaming(
499
+ ruby: &Ruby,
500
+ this: &Self,
501
+ path: String,
502
+ request: RString,
503
+ metadata: RHash,
504
+ timeout_ms: Option<u64>,
505
+ ) -> Result<ResponseStream, Error> {
506
+ let (runtime, args) = Self::call_args(ruby, this, path, request, metadata, timeout_ms)?;
507
+ let timeout = args.timeout;
508
+ let (tx, rx) = mpsc::channel(STREAM_BUFFER);
509
+ let outcome = Arc::new(Mutex::new(None));
510
+ let slot = outcome.clone();
511
+ let task = runtime.spawn(async move {
512
+ let result = with_timeout(server_stream(args, tx.clone()), timeout).await;
513
+ // Set before `tx` is dropped, so the reader finds it at the end
514
+ // of the stream.
515
+ *slot.lock() = Some(result);
516
+ drop(tx);
517
+ });
518
+ Ok(ResponseStream {
519
+ pid: std::process::id(),
520
+ rx: Mutex::new(Some(rx)),
521
+ outcome,
522
+ task: Mutex::new(Some(AbortOnDrop(task))),
523
+ })
524
+ }
525
+ }
526
+
527
+ // ─── Response stream ──────────────────────────────────────────────
528
+
529
+ /// Messages read ahead of a slow reader. Past this, HTTP/2 flow control
530
+ /// holds the server back.
531
+ const STREAM_BUFFER: usize = 8;
532
+
533
+ /// `TonicRails::Native::ResponseStream` — the messages of a
534
+ /// server-streaming call, then its status.
535
+ #[derive(magnus::TypedData)]
536
+ #[magnus(class = "TonicRails::Native::ResponseStream", free_immediately)]
537
+ pub struct ResponseStream {
538
+ /// The process that started the call.
539
+ pid: u32,
540
+ rx: Mutex<Option<mpsc::Receiver<Bytes>>>,
541
+ outcome: Arc<Mutex<Option<Outcome>>>,
542
+ task: Mutex<Option<AbortOnDrop<()>>>,
543
+ }
544
+
545
+ impl magnus::typed_data::DataTypeFunctions for ResponseStream {}
546
+
547
+ impl ResponseStream {
548
+ /// Next raw message, or nil once the stream has ended (see `#result`).
549
+ pub fn next(ruby: &Ruby, this: &Self) -> Result<Option<RString>, Error> {
550
+ // Taken out of the mutex while blocked so `#close` from another
551
+ // thread doesn't wait on it.
552
+ let Some(mut rx) = this.rx.lock().take() else {
553
+ return Ok(None);
554
+ };
555
+ loop {
556
+ match recv_blocking(&mut rx) {
557
+ Wait::Ready(bytes) => {
558
+ this.rx.lock().replace(rx);
559
+ return Ok(Some(ruby.str_from_slice(&bytes)));
560
+ }
561
+ Wait::Closed => return Ok(None),
562
+ Wait::Interrupted => {
563
+ if let Err(e) = ruby.thread_check_ints() {
564
+ this.rx.lock().replace(rx);
565
+ return Err(e);
566
+ }
567
+ }
568
+ }
569
+ }
570
+ }
571
+
572
+ /// How the call ended, as described in [`Outcome::into_ruby`], once
573
+ /// `#next` has returned nil.
574
+ pub fn result(ruby: &Ruby, this: &Self) -> Result<RArray, Error> {
575
+ let outcome = this
576
+ .outcome
577
+ .lock()
578
+ .take()
579
+ .unwrap_or_else(|| Outcome::error(Code::Cancelled, "the stream was closed before it ended"));
580
+ outcome.into_ruby(ruby)
581
+ }
582
+
583
+ /// Stop reading. A call still running is cancelled on the server.
584
+ pub fn close(&self) {
585
+ if self.pid != std::process::id() {
586
+ // Started by the parent: see the module docs.
587
+ return;
588
+ }
589
+ drop(self.task.lock().take());
590
+ drop(self.rx.lock().take());
591
+ }
592
+ }
593
+
594
+ impl Drop for ResponseStream {
595
+ fn drop(&mut self) {
596
+ if self.pid != std::process::id() {
597
+ // The task and channel belong to the parent's runtime.
598
+ std::mem::forget(self.task.get_mut().take());
599
+ std::mem::forget(self.rx.get_mut().take());
600
+ }
601
+ }
602
+ }
603
+
604
+ /// The system's root certificates as a PEM bundle, loaded on first use.
605
+ fn system_roots() -> Result<&'static [u8], String> {
606
+ static ROOTS: OnceLock<Result<Vec<u8>, String>> = OnceLock::new();
607
+ let roots = ROOTS.get_or_init(|| {
608
+ let loaded = rustls_native_certs::load_native_certs();
609
+ if loaded.certs.is_empty() {
610
+ let cause = loaded.errors.first().map(|e| format!(" ({e})")).unwrap_or_default();
611
+ return Err(format!(
612
+ "no system root certificates found{cause}; install them (e.g. the ca-certificates package) or pass tls_ca_path"
613
+ ));
614
+ }
615
+ let mut pem = Vec::new();
616
+ for cert in loaded.certs {
617
+ pem.extend_from_slice(b"-----BEGIN CERTIFICATE-----\n");
618
+ let encoded = base64::engine::general_purpose::STANDARD.encode(cert.as_ref());
619
+ for line in encoded.as_bytes().chunks(64) {
620
+ pem.extend_from_slice(line);
621
+ pem.push(b'\n');
622
+ }
623
+ pem.extend_from_slice(b"-----END CERTIFICATE-----\n");
624
+ }
625
+ Ok(pem)
626
+ });
627
+ roots.as_deref().map_err(Clone::clone)
628
+ }
629
+
630
+ /// Request metadata from a Hash of wire-form names and values (see
631
+ /// `Tonic::Rails::Metadata.encode`).
632
+ fn header_map(ruby: &Ruby, metadata: RHash) -> Result<HeaderMap, Error> {
633
+ let mut headers = HeaderMap::with_capacity(metadata.len());
634
+ metadata.foreach(|name: String, value: String| {
635
+ let invalid = || {
636
+ Error::new(
637
+ ruby.exception_arg_error(),
638
+ format!("invalid gRPC metadata {name:?}"),
639
+ )
640
+ };
641
+ let header = HeaderName::from_bytes(name.as_bytes()).map_err(|_| invalid())?;
642
+ let value = HeaderValue::from_str(&value).map_err(|_| invalid())?;
643
+ headers.append(header, value);
644
+ Ok(ForEach::Continue)
645
+ })?;
646
+ Ok(headers)
647
+ }
648
+
649
+ /// tonic's errors wrap the useful message (e.g. "no valid private key")
650
+ /// a level or two down.
651
+ fn source_chain(error: &dyn std::error::Error) -> String {
652
+ append_causes(error.to_string(), error.source())
653
+ }
654
+
655
+ /// Append each cause in the chain that the message doesn't already include.
656
+ fn append_causes(mut message: String, mut source: Option<&dyn std::error::Error>) -> String {
657
+ while let Some(e) = source {
658
+ let cause = e.to_string();
659
+ if !message.contains(&cause) {
660
+ message = format!("{message}: {cause}");
661
+ }
662
+ source = e.source();
663
+ }
664
+ message
665
+ }
@@ -23,8 +23,8 @@ use tonic::Code;
23
23
  use tonic::body::Body as TonicBody;
24
24
  use tonic::transport::server::{TcpConnectInfo, TlsConnectInfo};
25
25
 
26
- use crate::body::{ErrorBody, ResponseBody, ServerStats};
27
- use crate::framing::{MessageReader, append_metadata, parse_grpc_timeout};
26
+ use crate::body::{ResponseBody, ServerStats};
27
+ use crate::framing::{MessageReader, append_metadata, parse_grpc_timeout, status_trailers};
28
28
  use crate::gvl::{DispatchContext, Peer, RequestError, StreamMessage};
29
29
  use crate::log::Level;
30
30
 
@@ -84,9 +84,16 @@ fn grpc_response(body: TonicBody, compressed: bool) -> Response<TonicBody> {
84
84
  }
85
85
 
86
86
  impl DispatchInner {
87
+ /// A Trailers-Only response (the status in the headers, no body) for a
88
+ /// call rejected before it reached a handler. Clients may retry those
89
+ /// safely, and gRPC retries count on the form to tell them apart.
87
90
  fn reject(&self, path: &str, start: Instant, code: Code, message: &str) -> Response<TonicBody> {
88
91
  self.stats.record(path, code as u32, message, start, true);
89
- grpc_response(TonicBody::new(ErrorBody::new(code, message)), false)
92
+ let mut response = grpc_response(TonicBody::empty(), false);
93
+ response
94
+ .headers_mut()
95
+ .extend(status_trailers(code as u32, message));
96
+ response
90
97
  }
91
98
 
92
99
  async fn handle(&self, req: Request<axum::body::Body>) -> Response<TonicBody> {