tonic-rails 0.4.0 → 0.6.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +44 -1
- data/Cargo.lock +69 -1
- data/README.md +257 -8
- data/ext/tonic_rails_native/Cargo.toml +9 -2
- data/ext/tonic_rails_native/src/client.rs +655 -0
- data/ext/tonic_rails_native/src/gvl.rs +22 -7
- data/ext/tonic_rails_native/src/lib.rs +17 -0
- data/ext/tonic_rails_native/src/net.rs +121 -6
- data/ext/tonic_rails_native/src/server.rs +42 -4
- data/lib/puma/plugin/tonic_rails.rb +5 -3
- data/lib/tonic/rails/base.rb +4 -1
- data/lib/tonic/rails/client.rb +371 -0
- data/lib/tonic/rails/cluster.rb +222 -0
- data/lib/tonic/rails/configuration.rb +32 -0
- data/lib/tonic/rails/instrumentation.rb +19 -4
- data/lib/tonic/rails/railtie.rb +5 -0
- data/lib/tonic/rails/rpc_error.rb +61 -0
- data/lib/tonic/rails/server.rb +74 -32
- data/lib/tonic/rails/version.rb +1 -1
- data/lib/tonic-rails.rb +2 -0
- metadata +4 -1
|
@@ -20,7 +20,7 @@ use magnus::{
|
|
|
20
20
|
Error, RHash, RString, Ruby, Value, prelude::*, r_hash::ForEach, typed_data::DataTypeFunctions,
|
|
21
21
|
value::Opaque,
|
|
22
22
|
};
|
|
23
|
-
use parking_lot::Mutex;
|
|
23
|
+
use parking_lot::{Condvar, Mutex};
|
|
24
24
|
use rb_sys::{RB_NOGVL_INTR_FAIL, rb_nogvl, rb_thread_call_with_gvl};
|
|
25
25
|
use tokio::sync::{Notify, mpsc};
|
|
26
26
|
|
|
@@ -100,25 +100,40 @@ where
|
|
|
100
100
|
|
|
101
101
|
/// Minimal single-future executor that parks the current (non-tokio)
|
|
102
102
|
/// thread. tokio's channel and `Notify` futures are runtime-agnostic.
|
|
103
|
+
///
|
|
104
|
+
/// It parks on a pthread condition variable rather than
|
|
105
|
+
/// `std::thread::park`, which on macOS uses libdispatch: libdispatch
|
|
106
|
+
/// traps in the child of a multi-threaded process, and the client blocks
|
|
107
|
+
/// here in processes forked after it made calls.
|
|
103
108
|
pub fn block_on<F: Future>(fut: F) -> F::Output {
|
|
104
|
-
|
|
105
|
-
|
|
109
|
+
#[derive(Default)]
|
|
110
|
+
struct Signal {
|
|
111
|
+
woken: Mutex<bool>,
|
|
112
|
+
condvar: Condvar,
|
|
113
|
+
}
|
|
114
|
+
impl Wake for Signal {
|
|
106
115
|
fn wake(self: Arc<Self>) {
|
|
107
|
-
self.
|
|
116
|
+
self.wake_by_ref();
|
|
108
117
|
}
|
|
109
118
|
fn wake_by_ref(self: &Arc<Self>) {
|
|
110
|
-
self.
|
|
119
|
+
*self.woken.lock() = true;
|
|
120
|
+
self.condvar.notify_one();
|
|
111
121
|
}
|
|
112
122
|
}
|
|
113
123
|
|
|
114
|
-
let
|
|
124
|
+
let signal = Arc::new(Signal::default());
|
|
125
|
+
let waker = Waker::from(signal.clone());
|
|
115
126
|
let mut cx = Context::from_waker(&waker);
|
|
116
127
|
let mut fut = std::pin::pin!(fut);
|
|
117
128
|
loop {
|
|
118
129
|
if let Poll::Ready(value) = fut.as_mut().poll(&mut cx) {
|
|
119
130
|
return value;
|
|
120
131
|
}
|
|
121
|
-
|
|
132
|
+
let mut woken = signal.woken.lock();
|
|
133
|
+
while !*woken {
|
|
134
|
+
signal.condvar.wait(&mut woken);
|
|
135
|
+
}
|
|
136
|
+
*woken = false;
|
|
122
137
|
}
|
|
123
138
|
}
|
|
124
139
|
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
//! Modules:
|
|
21
21
|
//! - [`framing`] — gRPC wire format (frames, gzip, timeouts, trailers)
|
|
22
22
|
//! - [`body`] — response bodies and request stats
|
|
23
|
+
//! - [`client`] — `TonicRails::Native::Channel` and `ResponseStream`, for outgoing calls
|
|
23
24
|
//! - [`dispatch`] — the fallback tower service that queues RPCs for Ruby
|
|
24
25
|
//! - [`gvl`] — GVL release/acquire and the Ruby stream objects
|
|
25
26
|
//! - [`log`] — transport log events for Ruby's logger
|
|
@@ -27,6 +28,7 @@
|
|
|
27
28
|
//! - [`server`] — `TonicRails::Native::Server`
|
|
28
29
|
|
|
29
30
|
mod body;
|
|
31
|
+
mod client;
|
|
30
32
|
mod dispatch;
|
|
31
33
|
mod framing;
|
|
32
34
|
mod gvl;
|
|
@@ -36,6 +38,7 @@ mod server;
|
|
|
36
38
|
|
|
37
39
|
use magnus::{Error, Ruby, function, method, prelude::*};
|
|
38
40
|
|
|
41
|
+
use client::{NativeChannel, ResponseStream};
|
|
39
42
|
use gvl::{RequestReader, StreamSender};
|
|
40
43
|
use server::NativeServer;
|
|
41
44
|
|
|
@@ -43,6 +46,8 @@ use server::NativeServer;
|
|
|
43
46
|
fn init(ruby: &Ruby) -> Result<(), Error> {
|
|
44
47
|
let native = ruby.define_module("TonicRails")?.define_module("Native")?;
|
|
45
48
|
|
|
49
|
+
native.define_module_function("bind_listener", function!(server::bind_listener, 1))?;
|
|
50
|
+
|
|
46
51
|
let class = native.define_class("Server", ruby.class_object())?;
|
|
47
52
|
class.define_singleton_method("new", function!(NativeServer::new, 2))?;
|
|
48
53
|
class.define_method("on_request", method!(NativeServer::on_request, 1))?;
|
|
@@ -66,6 +71,7 @@ fn init(ruby: &Ruby) -> Result<(), Error> {
|
|
|
66
71
|
class.define_method("set_reflection", method!(NativeServer::set_reflection, 1))?;
|
|
67
72
|
class.define_method("set_grpc_web", method!(NativeServer::set_grpc_web, 3))?;
|
|
68
73
|
class.define_method("set_routes", method!(NativeServer::set_routes, 1))?;
|
|
74
|
+
class.define_method("set_listener_fd", method!(NativeServer::set_listener_fd, 1))?;
|
|
69
75
|
class.define_method(
|
|
70
76
|
"set_reflection_descriptors",
|
|
71
77
|
method!(NativeServer::set_reflection_descriptors, 1),
|
|
@@ -92,5 +98,16 @@ fn init(ruby: &Ruby) -> Result<(), Error> {
|
|
|
92
98
|
reader.define_method("next", method!(RequestReader::next, 0))?;
|
|
93
99
|
reader.define_method("error", method!(RequestReader::error, 0))?;
|
|
94
100
|
|
|
101
|
+
let channel = native.define_class("Channel", ruby.class_object())?;
|
|
102
|
+
channel.define_singleton_method("new", function!(NativeChannel::new, 4))?;
|
|
103
|
+
channel.define_method("unary", method!(NativeChannel::unary, 4))?;
|
|
104
|
+
channel.define_method("server_streaming", method!(NativeChannel::server_streaming, 4))?;
|
|
105
|
+
|
|
106
|
+
let stream = native.define_class("ResponseStream", ruby.class_object())?;
|
|
107
|
+
stream.undef_default_alloc_func();
|
|
108
|
+
stream.define_method("next", method!(ResponseStream::next, 0))?;
|
|
109
|
+
stream.define_method("result", method!(ResponseStream::result, 0))?;
|
|
110
|
+
stream.define_method("close", method!(ResponseStream::close, 0))?;
|
|
111
|
+
|
|
95
112
|
Ok(())
|
|
96
113
|
}
|
|
@@ -9,17 +9,44 @@ use std::time::Duration;
|
|
|
9
9
|
|
|
10
10
|
use socket2::{Domain, Protocol, Socket, Type};
|
|
11
11
|
use tokio::io::{AsyncRead, AsyncWrite, ReadBuf};
|
|
12
|
-
use tokio::net::
|
|
12
|
+
use tokio::net::TcpStream;
|
|
13
13
|
use tokio::sync::{OwnedSemaphorePermit, Semaphore};
|
|
14
14
|
use tokio_stream::Stream;
|
|
15
15
|
use tonic::transport::server::{Connected, TcpConnectInfo};
|
|
16
16
|
|
|
17
17
|
use crate::log::{Level, LogSink};
|
|
18
18
|
|
|
19
|
-
///
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
19
|
+
/// A stream of accepted connections.
|
|
20
|
+
pub type Accept = Pin<Box<dyn Stream<Item = std::io::Result<TcpStream>> + Send>>;
|
|
21
|
+
|
|
22
|
+
/// Bind `addr` and accept on it. Must be called inside a tokio runtime
|
|
23
|
+
/// context.
|
|
24
|
+
pub fn listen(addr: SocketAddr, backlog: i32) -> std::io::Result<Accept> {
|
|
25
|
+
let listener = tokio::net::TcpListener::from_std(bind(addr, backlog)?)?;
|
|
26
|
+
Ok(Box::pin(tokio_stream::wrappers::TcpListenerStream::new(listener)))
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/// Accept on a listening socket inherited from a parent process, shared
|
|
30
|
+
/// with sibling workers (multi-process mode). The fd is duplicated, so
|
|
31
|
+
/// the caller keeps ownership of the original. `active` is this
|
|
32
|
+
/// process's open connection count. Must be called inside a tokio
|
|
33
|
+
/// runtime context.
|
|
34
|
+
#[cfg(unix)]
|
|
35
|
+
pub fn listen_inherited(fd: i32, active: Arc<AtomicUsize>) -> std::io::Result<Accept> {
|
|
36
|
+
Ok(Box::pin(SharedAccept::new(inherited(fd)?, active)?))
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
#[cfg(not(unix))]
|
|
40
|
+
pub fn listen_inherited(_fd: i32, _active: Arc<AtomicUsize>) -> std::io::Result<Accept> {
|
|
41
|
+
Err(std::io::Error::new(
|
|
42
|
+
std::io::ErrorKind::Unsupported,
|
|
43
|
+
"inherited listeners need fork(), which this platform lacks",
|
|
44
|
+
))
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/// Bind with SO_REUSEADDR + SO_REUSEPORT so restarts don't fail on
|
|
48
|
+
/// TIME_WAIT. The listener is non-blocking, ready for tokio.
|
|
49
|
+
pub fn bind(addr: SocketAddr, backlog: i32) -> std::io::Result<std::net::TcpListener> {
|
|
23
50
|
let domain = if addr.is_ipv4() {
|
|
24
51
|
Domain::IPV4
|
|
25
52
|
} else {
|
|
@@ -35,7 +62,95 @@ pub fn bind(addr: SocketAddr, backlog: i32) -> std::io::Result<TcpListener> {
|
|
|
35
62
|
socket.set_nonblocking(true)?;
|
|
36
63
|
socket.bind(&addr.into())?;
|
|
37
64
|
socket.listen(backlog)?;
|
|
38
|
-
|
|
65
|
+
Ok(socket.into())
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
#[cfg(unix)]
|
|
69
|
+
fn inherited(fd: i32) -> std::io::Result<std::net::TcpListener> {
|
|
70
|
+
use std::os::fd::BorrowedFd;
|
|
71
|
+
|
|
72
|
+
// SAFETY: the caller guarantees `fd` is open for the duration of
|
|
73
|
+
// this call; we only duplicate it.
|
|
74
|
+
let owned = unsafe { BorrowedFd::borrow_raw(fd) }.try_clone_to_owned()?;
|
|
75
|
+
let socket = Socket::from(owned);
|
|
76
|
+
socket.set_nonblocking(true)?;
|
|
77
|
+
Ok(socket.into())
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/// Accepts on a listening socket shared with sibling worker processes.
|
|
81
|
+
///
|
|
82
|
+
/// Every process is woken for each new connection and the first to
|
|
83
|
+
/// accept wins, so gRPC's few long-lived connections tend to pile onto
|
|
84
|
+
/// whichever process wakes fastest. Instead, when a connection is
|
|
85
|
+
/// waiting, a process holds back for `backoff` per connection it already
|
|
86
|
+
/// has (up to `max_backoff`) before trying to accept, so the least busy
|
|
87
|
+
/// process usually gets it, as with Puma's `wait_for_less_busy_worker`.
|
|
88
|
+
#[cfg(unix)]
|
|
89
|
+
struct SharedAccept {
|
|
90
|
+
listener: tokio::io::unix::AsyncFd<std::net::TcpListener>,
|
|
91
|
+
active: Arc<AtomicUsize>,
|
|
92
|
+
backoff: Duration,
|
|
93
|
+
max_backoff: Duration,
|
|
94
|
+
pause: Option<Pin<Box<tokio::time::Sleep>>>,
|
|
95
|
+
/// The pause for the waiting connection is over: accept right away.
|
|
96
|
+
paused: bool,
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
#[cfg(unix)]
|
|
100
|
+
impl SharedAccept {
|
|
101
|
+
fn new(listener: std::net::TcpListener, active: Arc<AtomicUsize>) -> std::io::Result<Self> {
|
|
102
|
+
Ok(Self {
|
|
103
|
+
listener: tokio::io::unix::AsyncFd::new(listener)?,
|
|
104
|
+
active,
|
|
105
|
+
backoff: Duration::from_millis(1),
|
|
106
|
+
max_backoff: Duration::from_millis(10),
|
|
107
|
+
pause: None,
|
|
108
|
+
paused: false,
|
|
109
|
+
})
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
#[cfg(unix)]
|
|
114
|
+
impl Stream for SharedAccept {
|
|
115
|
+
type Item = std::io::Result<TcpStream>;
|
|
116
|
+
|
|
117
|
+
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
|
118
|
+
let this = &mut *self;
|
|
119
|
+
loop {
|
|
120
|
+
if let Some(pause) = this.pause.as_mut() {
|
|
121
|
+
if pause.as_mut().poll(cx).is_pending() {
|
|
122
|
+
return Poll::Pending;
|
|
123
|
+
}
|
|
124
|
+
this.pause = None;
|
|
125
|
+
this.paused = true;
|
|
126
|
+
}
|
|
127
|
+
let mut guard = match this.listener.poll_read_ready(cx) {
|
|
128
|
+
Poll::Ready(Ok(guard)) => guard,
|
|
129
|
+
Poll::Ready(Err(e)) => return Poll::Ready(Some(Err(e))),
|
|
130
|
+
Poll::Pending => return Poll::Pending,
|
|
131
|
+
};
|
|
132
|
+
let active = this.active.load(Ordering::Relaxed);
|
|
133
|
+
if !this.paused && active > 0 {
|
|
134
|
+
let pause = this.backoff.saturating_mul(active as u32).min(this.max_backoff);
|
|
135
|
+
this.pause = Some(Box::pin(tokio::time::sleep(pause)));
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
// Whether we accept or a sibling beat us to it, the next
|
|
139
|
+
// connection gets a fresh pause.
|
|
140
|
+
this.paused = false;
|
|
141
|
+
match guard.try_io(|listener| listener.get_ref().accept()) {
|
|
142
|
+
Ok(Ok((stream, _))) => {
|
|
143
|
+
return Poll::Ready(Some(
|
|
144
|
+
stream
|
|
145
|
+
.set_nonblocking(true)
|
|
146
|
+
.and_then(|()| TcpStream::from_std(stream)),
|
|
147
|
+
));
|
|
148
|
+
}
|
|
149
|
+
Ok(Err(e)) => return Poll::Ready(Some(Err(e))),
|
|
150
|
+
Err(_would_block) => continue,
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
39
154
|
}
|
|
40
155
|
|
|
41
156
|
/// TCP_NODELAY for latency; TCP keepalive to reap half-open peers that
|
|
@@ -36,7 +36,7 @@ use crate::body::ServerStats;
|
|
|
36
36
|
use crate::dispatch::{DispatchInner, DispatchService};
|
|
37
37
|
use crate::gvl::{self, Cancel, DispatchContext};
|
|
38
38
|
use crate::log::{Level, LogEvent, LogSink};
|
|
39
|
-
use crate::net::{LimitedIncoming, bind};
|
|
39
|
+
use crate::net::{LimitedIncoming, bind, listen, listen_inherited};
|
|
40
40
|
|
|
41
41
|
/// The overall-server health entry (empty service name).
|
|
42
42
|
const OVERALL: &str = "";
|
|
@@ -54,6 +54,8 @@ struct Options {
|
|
|
54
54
|
dispatch_queue_size: usize,
|
|
55
55
|
routes: Vec<String>,
|
|
56
56
|
descriptors: Vec<Vec<u8>>,
|
|
57
|
+
/// Accept on this inherited listening socket instead of binding.
|
|
58
|
+
listener_fd: Option<i32>,
|
|
57
59
|
}
|
|
58
60
|
|
|
59
61
|
impl Default for Options {
|
|
@@ -71,6 +73,7 @@ impl Default for Options {
|
|
|
71
73
|
dispatch_queue_size: 128,
|
|
72
74
|
routes: Vec::new(),
|
|
73
75
|
descriptors: Vec::new(),
|
|
76
|
+
listener_fd: None,
|
|
74
77
|
}
|
|
75
78
|
}
|
|
76
79
|
}
|
|
@@ -191,6 +194,29 @@ fn read_file(ruby: &Ruby, what: &str, path: &str) -> Result<Vec<u8>, Error> {
|
|
|
191
194
|
std::fs::read(path).map_err(|e| runtime_error(ruby, format!("cannot read TLS {what} {path}: {e}")))
|
|
192
195
|
}
|
|
193
196
|
|
|
197
|
+
/// `TonicRails::Native.bind_listener(address)`: bind and listen the way
|
|
198
|
+
/// the server does, returning the socket's fd for worker processes to
|
|
199
|
+
/// inherit and accept on (`Server#set_listener_fd`). The caller owns it.
|
|
200
|
+
#[cfg(unix)]
|
|
201
|
+
pub fn bind_listener(ruby: &Ruby, address: String) -> Result<i32, Error> {
|
|
202
|
+
use std::os::fd::IntoRawFd;
|
|
203
|
+
|
|
204
|
+
let addr: SocketAddr = address
|
|
205
|
+
.parse()
|
|
206
|
+
.map_err(|e| arg_error(ruby, format!("invalid bind address {address:?}: {e}")))?;
|
|
207
|
+
let listener =
|
|
208
|
+
bind(addr, 1024).map_err(|e| runtime_error(ruby, format!("failed to bind {addr}: {e}")))?;
|
|
209
|
+
Ok(listener.into_raw_fd())
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
#[cfg(not(unix))]
|
|
213
|
+
pub fn bind_listener(ruby: &Ruby, _address: String) -> Result<i32, Error> {
|
|
214
|
+
Err(Error::new(
|
|
215
|
+
ruby.exception_not_imp_error(),
|
|
216
|
+
"multi-process mode needs fork(), which this platform lacks",
|
|
217
|
+
))
|
|
218
|
+
}
|
|
219
|
+
|
|
194
220
|
fn secs(value: f64) -> Duration {
|
|
195
221
|
Duration::from_secs_f64(value.max(0.0))
|
|
196
222
|
}
|
|
@@ -292,6 +318,12 @@ impl NativeServer {
|
|
|
292
318
|
self.options.lock().reflection = enabled;
|
|
293
319
|
}
|
|
294
320
|
|
|
321
|
+
/// Accept on a listening socket bound by a parent process (see
|
|
322
|
+
/// `bind_listener`) instead of binding `bind_address`.
|
|
323
|
+
pub fn set_listener_fd(&self, fd: i32) {
|
|
324
|
+
self.options.lock().listener_fd = Some(fd);
|
|
325
|
+
}
|
|
326
|
+
|
|
295
327
|
/// gRPC paths (`/package.Service/Method`) served by Ruby handlers.
|
|
296
328
|
pub fn set_routes(&self, routes: Vec<String>) {
|
|
297
329
|
self.options.lock().routes = routes;
|
|
@@ -387,9 +419,15 @@ impl NativeServer {
|
|
|
387
419
|
.build()
|
|
388
420
|
.map_err(|e| runtime_error(ruby, format!("failed to create tokio runtime: {e}")))?;
|
|
389
421
|
|
|
390
|
-
let
|
|
422
|
+
let accept = {
|
|
391
423
|
let _guard = runtime.enter();
|
|
392
|
-
|
|
424
|
+
match options.listener_fd {
|
|
425
|
+
Some(fd) => listen_inherited(fd, this.active_connections.clone()).map_err(|e| {
|
|
426
|
+
runtime_error(ruby, format!("cannot use inherited listener (fd {fd}): {e}"))
|
|
427
|
+
})?,
|
|
428
|
+
None => listen(addr, 1024)
|
|
429
|
+
.map_err(|e| runtime_error(ruby, format!("failed to bind {addr}: {e}")))?,
|
|
430
|
+
}
|
|
393
431
|
};
|
|
394
432
|
|
|
395
433
|
let (health, health_service) = tonic_health::server::health_reporter();
|
|
@@ -453,7 +491,7 @@ impl NativeServer {
|
|
|
453
491
|
n => n,
|
|
454
492
|
};
|
|
455
493
|
let incoming = LimitedIncoming {
|
|
456
|
-
inner:
|
|
494
|
+
inner: accept,
|
|
457
495
|
semaphore: Arc::new(Semaphore::new(permits)),
|
|
458
496
|
active: this.active_connections.clone(),
|
|
459
497
|
log: this.stats.log.clone(),
|
|
@@ -9,7 +9,8 @@ require "puma/plugin"
|
|
|
9
9
|
#
|
|
10
10
|
# The gRPC server runs as one child process of the Puma master, in single
|
|
11
11
|
# and cluster mode alike, so it has its own GVL and never competes with web
|
|
12
|
-
# workers.
|
|
12
|
+
# workers. With `config.processes > 1`, that child supervises the gRPC
|
|
13
|
+
# worker processes. It starts once Puma has booted, drains gracefully when Puma
|
|
13
14
|
# stops or restarts, stops itself if Puma goes away, and if it dies
|
|
14
15
|
# unexpectedly Puma shuts down too, so a supervisor can restart both.
|
|
15
16
|
Puma::Plugin.create do
|
|
@@ -62,7 +63,8 @@ Puma::Plugin.create do
|
|
|
62
63
|
end
|
|
63
64
|
|
|
64
65
|
# Puma fires its stop hooks from its TERM trap handler, so this must
|
|
65
|
-
# not block indefinitely: allow the server's drain timeout plus a margin
|
|
66
|
+
# not block indefinitely: allow the server's drain timeout plus a margin
|
|
67
|
+
# (longer than the one a multi-process server gives its own workers),
|
|
66
68
|
# then kill it.
|
|
67
69
|
def stop_grpc
|
|
68
70
|
pid = @grpc_pid
|
|
@@ -87,7 +89,7 @@ Puma::Plugin.create do
|
|
|
87
89
|
|
|
88
90
|
def stop_timeout
|
|
89
91
|
drain = defined?(Tonic::Rails) ? Tonic::Rails.configuration.drain_timeout.to_f : 10.0
|
|
90
|
-
drain +
|
|
92
|
+
drain + 10
|
|
91
93
|
end
|
|
92
94
|
|
|
93
95
|
def monitor_grpc
|
data/lib/tonic/rails/base.rb
CHANGED
|
@@ -248,7 +248,10 @@ module Tonic
|
|
|
248
248
|
|
|
249
249
|
@request = build_request(reader, spec)
|
|
250
250
|
response = nil
|
|
251
|
-
|
|
251
|
+
# Calls this handler makes with Tonic::Rails::Client default to its deadline.
|
|
252
|
+
Client.with_deadline(@grpc_deadline_at) do
|
|
253
|
+
run_callbacks(:rpc) { response = respond(public_send(spec[:handler], @request), spec, sender) }
|
|
254
|
+
end
|
|
252
255
|
response
|
|
253
256
|
rescue RpcError
|
|
254
257
|
raise
|