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.
@@ -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
- struct ThreadWaker(std::thread::Thread);
105
- impl Wake for ThreadWaker {
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.0.unpark();
116
+ self.wake_by_ref();
108
117
  }
109
118
  fn wake_by_ref(self: &Arc<Self>) {
110
- self.0.unpark();
119
+ *self.woken.lock() = true;
120
+ self.condvar.notify_one();
111
121
  }
112
122
  }
113
123
 
114
- let waker = Waker::from(Arc::new(ThreadWaker(std::thread::current())));
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
- std::thread::park();
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::{TcpListener, TcpStream};
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
- /// Bind with SO_REUSEADDR + SO_REUSEPORT so several worker processes can
20
- /// share a port (the kernel load-balances accepts) and restarts don't
21
- /// fail on TIME_WAIT. Must be called inside a tokio runtime context.
22
- pub fn bind(addr: SocketAddr, backlog: i32) -> std::io::Result<TcpListener> {
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
- TcpListener::from_std(socket.into())
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 listener = {
422
+ let accept = {
391
423
  let _guard = runtime.enter();
392
- bind(addr, 1024).map_err(|e| runtime_error(ruby, format!("failed to bind {addr}: {e}")))?
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: tokio_stream::wrappers::TcpListenerStream::new(listener),
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. It starts once Puma has booted, drains gracefully when Puma
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 + 5
92
+ drain + 10
91
93
  end
92
94
 
93
95
  def monitor_grpc
@@ -248,7 +248,10 @@ module Tonic
248
248
 
249
249
  @request = build_request(reader, spec)
250
250
  response = nil
251
- run_callbacks(:rpc) { response = respond(public_send(spec[:handler], @request), spec, sender) }
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