tonic-rails 0.1.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 +7 -0
- data/CHANGELOG.md +52 -0
- data/Cargo.lock +1242 -0
- data/Cargo.toml +11 -0
- data/LICENSE +21 -0
- data/README.md +391 -0
- data/ext/tonic_rails_native/Cargo.toml +44 -0
- data/ext/tonic_rails_native/extconf.rb +9 -0
- data/ext/tonic_rails_native/src/body.rs +289 -0
- data/ext/tonic_rails_native/src/dispatch.rs +250 -0
- data/ext/tonic_rails_native/src/framing.rs +349 -0
- data/ext/tonic_rails_native/src/gvl.rs +473 -0
- data/ext/tonic_rails_native/src/lib.rs +90 -0
- data/ext/tonic_rails_native/src/net.rs +146 -0
- data/ext/tonic_rails_native/src/server.rs +507 -0
- data/lib/tonic/rails/base.rb +215 -0
- data/lib/tonic/rails/configuration.rb +149 -0
- data/lib/tonic/rails/railtie.rb +56 -0
- data/lib/tonic/rails/reflection.rb +50 -0
- data/lib/tonic/rails/router.rb +71 -0
- data/lib/tonic/rails/rpc_error.rb +65 -0
- data/lib/tonic/rails/server.rb +224 -0
- data/lib/tonic/rails/tasks/tonic-rails.rake +36 -0
- data/lib/tonic/rails/testing.rb +85 -0
- data/lib/tonic/rails/version.rb +7 -0
- data/lib/tonic-rails.rb +91 -0
- metadata +121 -0
|
@@ -0,0 +1,473 @@
|
|
|
1
|
+
//! GVL bridge: releasing the GVL while blocked, re-acquiring it to run
|
|
2
|
+
//! Ruby handlers, and the Ruby-visible stream objects.
|
|
3
|
+
//!
|
|
4
|
+
//! Every blocking wait goes through [`without_gvl`], which uses
|
|
5
|
+
//! `rb_nogvl(..., RB_NOGVL_INTR_FAIL)`. Unlike `rb_thread_call_without_gvl`
|
|
6
|
+
//! this never raises (longjmps) through Rust frames: if the thread is
|
|
7
|
+
//! interrupted we return [`Wait::Interrupted`], the caller runs
|
|
8
|
+
//! `thread_check_ints` (which may run a trap handler or raise a proper
|
|
9
|
+
//! Ruby exception through Magnus), and then retries.
|
|
10
|
+
|
|
11
|
+
use std::ffi::c_void;
|
|
12
|
+
use std::future::Future;
|
|
13
|
+
use std::panic::{AssertUnwindSafe, catch_unwind, resume_unwind};
|
|
14
|
+
use std::sync::Arc;
|
|
15
|
+
use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
|
|
16
|
+
use std::task::{Context, Poll, Wake, Waker};
|
|
17
|
+
|
|
18
|
+
use magnus::{Error, RString, Ruby, Value, prelude::*, typed_data::DataTypeFunctions, value::Opaque};
|
|
19
|
+
use parking_lot::Mutex;
|
|
20
|
+
use rb_sys::{RB_NOGVL_INTR_FAIL, rb_nogvl, rb_thread_call_with_gvl};
|
|
21
|
+
use tokio::sync::{Notify, mpsc};
|
|
22
|
+
|
|
23
|
+
// ─── Blocking primitives ──────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
/// Cancellation token tripped by Ruby's unblocking function when the
|
|
26
|
+
/// waiting thread is interrupted (signal, `Thread#raise`, `Thread#kill`).
|
|
27
|
+
#[derive(Default)]
|
|
28
|
+
pub struct Cancel {
|
|
29
|
+
flag: AtomicBool,
|
|
30
|
+
notify: Notify,
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
impl Cancel {
|
|
34
|
+
pub fn cancel(&self) {
|
|
35
|
+
self.flag.store(true, Ordering::SeqCst);
|
|
36
|
+
// notify_one stores a permit if nobody is waiting yet, so a
|
|
37
|
+
// cancel that races ahead of `cancelled()` is never lost.
|
|
38
|
+
self.notify.notify_one();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
pub fn is_cancelled(&self) -> bool {
|
|
42
|
+
self.flag.load(Ordering::SeqCst)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
pub async fn cancelled(&self) {
|
|
46
|
+
while !self.is_cancelled() {
|
|
47
|
+
self.notify.notified().await;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
unsafe extern "C" fn cancel_ubf(data: *mut c_void) {
|
|
53
|
+
let cancel = unsafe { &*(data as *const Cancel) };
|
|
54
|
+
cancel.cancel();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/// Run `f` with the GVL released. Returns `None` without running `f` if
|
|
58
|
+
/// an interrupt was already pending. `f` must return promptly once
|
|
59
|
+
/// `cancel` is tripped.
|
|
60
|
+
pub fn without_gvl<F, R>(cancel: &Cancel, f: F) -> Option<R>
|
|
61
|
+
where
|
|
62
|
+
F: FnOnce() -> R,
|
|
63
|
+
{
|
|
64
|
+
struct Call<F, R> {
|
|
65
|
+
f: Option<F>,
|
|
66
|
+
result: Option<std::thread::Result<R>>,
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
unsafe extern "C" fn trampoline<F: FnOnce() -> R, R>(data: *mut c_void) -> *mut c_void {
|
|
70
|
+
let call = unsafe { &mut *(data as *mut Call<F, R>) };
|
|
71
|
+
if let Some(f) = call.f.take() {
|
|
72
|
+
call.result = Some(catch_unwind(AssertUnwindSafe(f)));
|
|
73
|
+
}
|
|
74
|
+
std::ptr::null_mut()
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
let mut call = Call {
|
|
78
|
+
f: Some(f),
|
|
79
|
+
result: None,
|
|
80
|
+
};
|
|
81
|
+
unsafe {
|
|
82
|
+
rb_nogvl(
|
|
83
|
+
Some(trampoline::<F, R>),
|
|
84
|
+
&mut call as *mut Call<F, R> as *mut c_void,
|
|
85
|
+
Some(cancel_ubf),
|
|
86
|
+
cancel as *const Cancel as *mut c_void,
|
|
87
|
+
RB_NOGVL_INTR_FAIL as i32,
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
match call.result {
|
|
91
|
+
Some(Ok(value)) => Some(value),
|
|
92
|
+
Some(Err(panic)) => resume_unwind(panic),
|
|
93
|
+
None => None,
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/// Minimal single-future executor that parks the current (non-tokio)
|
|
98
|
+
/// thread. tokio's channel and `Notify` futures are runtime-agnostic.
|
|
99
|
+
pub fn block_on<F: Future>(fut: F) -> F::Output {
|
|
100
|
+
struct ThreadWaker(std::thread::Thread);
|
|
101
|
+
impl Wake for ThreadWaker {
|
|
102
|
+
fn wake(self: Arc<Self>) {
|
|
103
|
+
self.0.unpark();
|
|
104
|
+
}
|
|
105
|
+
fn wake_by_ref(self: &Arc<Self>) {
|
|
106
|
+
self.0.unpark();
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
let waker = Waker::from(Arc::new(ThreadWaker(std::thread::current())));
|
|
111
|
+
let mut cx = Context::from_waker(&waker);
|
|
112
|
+
let mut fut = std::pin::pin!(fut);
|
|
113
|
+
loop {
|
|
114
|
+
if let Poll::Ready(value) = fut.as_mut().poll(&mut cx) {
|
|
115
|
+
return value;
|
|
116
|
+
}
|
|
117
|
+
std::thread::park();
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
pub enum Wait<T> {
|
|
122
|
+
Ready(T),
|
|
123
|
+
Closed,
|
|
124
|
+
Interrupted,
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/// Receive from a tokio channel, releasing the GVL only if we must block.
|
|
128
|
+
pub fn recv_blocking<T>(rx: &mut mpsc::Receiver<T>) -> Wait<T> {
|
|
129
|
+
match rx.try_recv() {
|
|
130
|
+
Ok(value) => return Wait::Ready(value),
|
|
131
|
+
Err(mpsc::error::TryRecvError::Disconnected) => return Wait::Closed,
|
|
132
|
+
Err(mpsc::error::TryRecvError::Empty) => {}
|
|
133
|
+
}
|
|
134
|
+
let cancel = Cancel::default();
|
|
135
|
+
let received = without_gvl(&cancel, || {
|
|
136
|
+
block_on(async {
|
|
137
|
+
tokio::select! {
|
|
138
|
+
biased;
|
|
139
|
+
value = rx.recv() => Some(value),
|
|
140
|
+
_ = cancel.cancelled() => None,
|
|
141
|
+
}
|
|
142
|
+
})
|
|
143
|
+
});
|
|
144
|
+
match received {
|
|
145
|
+
Some(Some(Some(value))) => Wait::Ready(value),
|
|
146
|
+
Some(Some(None)) => Wait::Closed,
|
|
147
|
+
_ => Wait::Interrupted,
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/// Send into a tokio channel, releasing the GVL only if we must block
|
|
152
|
+
/// (channel full — the client is consuming slowly). On `Interrupted`
|
|
153
|
+
/// the message is handed back for a retry.
|
|
154
|
+
pub fn send_blocking<T>(tx: &mpsc::Sender<T>, msg: T) -> Result<(), Option<T>> {
|
|
155
|
+
let msg = match tx.try_send(msg) {
|
|
156
|
+
Ok(()) => return Ok(()),
|
|
157
|
+
Err(mpsc::error::TrySendError::Closed(_)) => return Err(None),
|
|
158
|
+
Err(mpsc::error::TrySendError::Full(msg)) => msg,
|
|
159
|
+
};
|
|
160
|
+
let cancel = Cancel::default();
|
|
161
|
+
let mut pending = Some(msg);
|
|
162
|
+
let sent = without_gvl(&cancel, || {
|
|
163
|
+
block_on(async {
|
|
164
|
+
let permit = tokio::select! {
|
|
165
|
+
biased;
|
|
166
|
+
permit = tx.reserve() => permit.ok(),
|
|
167
|
+
_ = cancel.cancelled() => return None,
|
|
168
|
+
};
|
|
169
|
+
Some(permit.map(|p| p.send(pending.take().expect("message present"))))
|
|
170
|
+
})
|
|
171
|
+
});
|
|
172
|
+
match sent {
|
|
173
|
+
Some(Some(Some(()))) => Ok(()),
|
|
174
|
+
Some(Some(None)) => Err(None),
|
|
175
|
+
_ => Err(pending),
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/// `send_blocking`, retrying after Ruby has processed any interrupt.
|
|
180
|
+
/// Returns `Ok(false)` if the receiver is gone.
|
|
181
|
+
pub fn send_with_interrupts<T>(ruby: &Ruby, tx: &mpsc::Sender<T>, mut msg: T) -> Result<bool, Error> {
|
|
182
|
+
loop {
|
|
183
|
+
match send_blocking(tx, msg) {
|
|
184
|
+
Ok(()) => return Ok(true),
|
|
185
|
+
Err(None) => return Ok(false),
|
|
186
|
+
Err(Some(back)) => {
|
|
187
|
+
ruby.thread_check_ints()?;
|
|
188
|
+
msg = back;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// ─── Response stream ──────────────────────────────────────────────
|
|
195
|
+
|
|
196
|
+
/// Messages from the Ruby handler to the tokio response body.
|
|
197
|
+
pub enum StreamMessage {
|
|
198
|
+
/// One encoded protobuf response message.
|
|
199
|
+
Data(Vec<u8>),
|
|
200
|
+
/// End of stream: `None` = OK, `Some((code, message))` = error.
|
|
201
|
+
End(Option<(u32, String)>),
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/// `TonicRails::Native::StreamSender` — pushes encoded response messages
|
|
205
|
+
/// to the client with backpressure.
|
|
206
|
+
#[derive(magnus::TypedData)]
|
|
207
|
+
#[magnus(class = "TonicRails::Native::StreamSender", free_immediately)]
|
|
208
|
+
pub struct StreamSender {
|
|
209
|
+
tx: mpsc::Sender<StreamMessage>,
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
impl StreamSender {
|
|
213
|
+
/// Raises `IOError` if the client has gone away.
|
|
214
|
+
pub fn push(ruby: &Ruby, this: &Self, data: RString) -> Result<(), Error> {
|
|
215
|
+
let bytes = unsafe { data.as_slice() }.to_vec();
|
|
216
|
+
if send_with_interrupts(ruby, &this.tx, StreamMessage::Data(bytes))? {
|
|
217
|
+
Ok(())
|
|
218
|
+
} else {
|
|
219
|
+
Err(Error::new(
|
|
220
|
+
ruby.exception_io_error(),
|
|
221
|
+
"response stream closed by peer",
|
|
222
|
+
))
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
impl DataTypeFunctions for StreamSender {}
|
|
228
|
+
|
|
229
|
+
// ─── Request stream ───────────────────────────────────────────────
|
|
230
|
+
|
|
231
|
+
/// `TonicRails::Native::RequestReader` — yields raw request messages.
|
|
232
|
+
#[derive(magnus::TypedData)]
|
|
233
|
+
#[magnus(class = "TonicRails::Native::RequestReader", free_immediately)]
|
|
234
|
+
pub struct RequestReader {
|
|
235
|
+
/// Shared with the dispatcher, which closes it once the handler
|
|
236
|
+
/// returns so the body-reading task never outlives the call.
|
|
237
|
+
rx: Arc<Mutex<Option<mpsc::Receiver<Vec<u8>>>>>,
|
|
238
|
+
error: Arc<RequestError>,
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/// Set by the body-reading task when the request stream is invalid, so
|
|
242
|
+
/// the reader can tell an error-closed channel from a clean end of stream.
|
|
243
|
+
#[derive(Default)]
|
|
244
|
+
pub struct RequestError {
|
|
245
|
+
code: AtomicU32,
|
|
246
|
+
message: Mutex<&'static str>,
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
impl RequestError {
|
|
250
|
+
pub fn set(&self, code: tonic::Code, message: &'static str) {
|
|
251
|
+
*self.message.lock() = message;
|
|
252
|
+
self.code.store(code as u32, Ordering::Release);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
fn get(&self) -> Option<(u32, &'static str)> {
|
|
256
|
+
match self.code.load(Ordering::Acquire) {
|
|
257
|
+
0 => None,
|
|
258
|
+
code => Some((code, *self.message.lock())),
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
impl RequestReader {
|
|
264
|
+
fn new(rx: Arc<Mutex<Option<mpsc::Receiver<Vec<u8>>>>>, error: Arc<RequestError>) -> Self {
|
|
265
|
+
Self { rx, error }
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/// Next raw message, or nil at end of stream. Check `#error` after nil.
|
|
269
|
+
pub fn next(ruby: &Ruby, this: &Self) -> Result<Option<RString>, Error> {
|
|
270
|
+
// Taken out of the mutex while blocked so a concurrent caller
|
|
271
|
+
// (misuse) sees end-of-stream rather than deadlocking.
|
|
272
|
+
let Some(mut rx) = this.rx.lock().take() else {
|
|
273
|
+
return Ok(None);
|
|
274
|
+
};
|
|
275
|
+
loop {
|
|
276
|
+
match recv_blocking(&mut rx) {
|
|
277
|
+
Wait::Ready(bytes) => {
|
|
278
|
+
this.rx.lock().replace(rx);
|
|
279
|
+
return Ok(Some(ruby.str_from_slice(&bytes)));
|
|
280
|
+
}
|
|
281
|
+
Wait::Closed => return Ok(None),
|
|
282
|
+
Wait::Interrupted => {
|
|
283
|
+
if let Err(e) = ruby.thread_check_ints() {
|
|
284
|
+
this.rx.lock().replace(rx);
|
|
285
|
+
return Err(e);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/// `[code, message]` if the request stream ended with an error.
|
|
293
|
+
pub fn error(&self) -> Option<(u32, &'static str)> {
|
|
294
|
+
self.error.get()
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
impl DataTypeFunctions for RequestReader {}
|
|
299
|
+
|
|
300
|
+
// ─── Dispatch into Ruby ───────────────────────────────────────────
|
|
301
|
+
|
|
302
|
+
/// One RPC handed from tokio to a Ruby dispatch thread.
|
|
303
|
+
pub struct DispatchContext {
|
|
304
|
+
pub callback: Opaque<Value>,
|
|
305
|
+
pub path: String,
|
|
306
|
+
pub metadata: Vec<(String, String)>,
|
|
307
|
+
pub timeout_ms: Option<u128>,
|
|
308
|
+
pub response_tx: mpsc::Sender<StreamMessage>,
|
|
309
|
+
pub request_rx: mpsc::Receiver<Vec<u8>>,
|
|
310
|
+
pub request_error: Arc<RequestError>,
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/// Bounds on how long one thread keeps the GVL to drain a busy queue.
|
|
314
|
+
/// The time bound keeps CPU-heavy handlers from hurting tail latency:
|
|
315
|
+
/// only cheap requests get batched.
|
|
316
|
+
const MAX_BATCH: usize = 16;
|
|
317
|
+
const MAX_BATCH_TIME: std::time::Duration = std::time::Duration::from_micros(500);
|
|
318
|
+
|
|
319
|
+
/// Re-acquire the GVL and run the Ruby handler for `first`, then keep
|
|
320
|
+
/// the GVL to run requests already waiting in `queue` (within
|
|
321
|
+
/// `MAX_BATCH` / `MAX_BATCH_TIME`). Each GVL handoff costs a thread wake-up, so draining a
|
|
322
|
+
/// busy queue under one acquisition raises throughput. Handlers that
|
|
323
|
+
/// block on I/O still release the GVL internally, so this does not
|
|
324
|
+
/// serialise I/O-bound work. Called from a thread that has released
|
|
325
|
+
/// the GVL.
|
|
326
|
+
pub fn dispatch(first: Box<DispatchContext>, queue: &crossbeam_channel::Receiver<Box<DispatchContext>>) {
|
|
327
|
+
struct Batch<'a> {
|
|
328
|
+
first: Option<Box<DispatchContext>>,
|
|
329
|
+
queue: &'a crossbeam_channel::Receiver<Box<DispatchContext>>,
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
unsafe extern "C" fn with_gvl(data: *mut c_void) -> *mut c_void {
|
|
333
|
+
let batch = unsafe { &mut *(data as *mut Batch<'_>) };
|
|
334
|
+
let start = std::time::Instant::now();
|
|
335
|
+
let mut next = batch.first.take();
|
|
336
|
+
let mut handled = 0;
|
|
337
|
+
while let Some(ctx) = next {
|
|
338
|
+
run_guarded(*ctx);
|
|
339
|
+
handled += 1;
|
|
340
|
+
next = if handled < MAX_BATCH && start.elapsed() < MAX_BATCH_TIME {
|
|
341
|
+
batch.queue.try_recv().ok()
|
|
342
|
+
} else {
|
|
343
|
+
None
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
std::ptr::null_mut()
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
let mut batch = Batch {
|
|
350
|
+
first: Some(first),
|
|
351
|
+
queue,
|
|
352
|
+
};
|
|
353
|
+
unsafe {
|
|
354
|
+
rb_thread_call_with_gvl(Some(with_gvl), &mut batch as *mut Batch<'_> as *mut c_void);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
fn run_guarded(ctx: DispatchContext) {
|
|
359
|
+
let response_tx = ctx.response_tx.clone();
|
|
360
|
+
if let Err(panic) = catch_unwind(AssertUnwindSafe(|| run_handler(ctx))) {
|
|
361
|
+
let msg = panic
|
|
362
|
+
.downcast_ref::<String>()
|
|
363
|
+
.map(String::as_str)
|
|
364
|
+
.or_else(|| panic.downcast_ref::<&str>().copied())
|
|
365
|
+
.unwrap_or("unknown panic");
|
|
366
|
+
eprintln!("tonic-rails: panic in dispatch: {msg}");
|
|
367
|
+
let _ = response_tx.try_send(StreamMessage::End(Some((
|
|
368
|
+
tonic::Code::Internal as u32,
|
|
369
|
+
"internal error".into(),
|
|
370
|
+
))));
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
fn run_handler(ctx: DispatchContext) {
|
|
375
|
+
let ruby = Ruby::get().expect("dispatch runs on a Ruby thread with the GVL");
|
|
376
|
+
let DispatchContext {
|
|
377
|
+
callback,
|
|
378
|
+
path,
|
|
379
|
+
metadata,
|
|
380
|
+
timeout_ms,
|
|
381
|
+
response_tx,
|
|
382
|
+
request_rx,
|
|
383
|
+
request_error,
|
|
384
|
+
} = ctx;
|
|
385
|
+
|
|
386
|
+
let end = match call_handler(
|
|
387
|
+
&ruby,
|
|
388
|
+
callback,
|
|
389
|
+
&path,
|
|
390
|
+
metadata,
|
|
391
|
+
timeout_ms,
|
|
392
|
+
&response_tx,
|
|
393
|
+
request_rx,
|
|
394
|
+
request_error,
|
|
395
|
+
) {
|
|
396
|
+
Ok(()) => StreamMessage::End(None),
|
|
397
|
+
Err(e) => StreamMessage::End(Some(error_status(&e))),
|
|
398
|
+
};
|
|
399
|
+
// Ignore failures: the client may already be gone.
|
|
400
|
+
let _ = send_with_interrupts(&ruby, &response_tx, end);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
#[allow(clippy::too_many_arguments)]
|
|
404
|
+
fn call_handler(
|
|
405
|
+
ruby: &Ruby,
|
|
406
|
+
callback: Opaque<Value>,
|
|
407
|
+
path: &str,
|
|
408
|
+
metadata: Vec<(String, String)>,
|
|
409
|
+
timeout_ms: Option<u128>,
|
|
410
|
+
response_tx: &mpsc::Sender<StreamMessage>,
|
|
411
|
+
request_rx: mpsc::Receiver<Vec<u8>>,
|
|
412
|
+
request_error: Arc<RequestError>,
|
|
413
|
+
) -> Result<(), Error> {
|
|
414
|
+
let md = ruby.hash_new_capa(metadata.len());
|
|
415
|
+
for (k, v) in metadata {
|
|
416
|
+
md.aset(ruby.str_new(&k), ruby.str_new(&v))?;
|
|
417
|
+
}
|
|
418
|
+
let request_rx = Arc::new(Mutex::new(Some(request_rx)));
|
|
419
|
+
let call = ruby.hash_new_capa(3);
|
|
420
|
+
call.aset(
|
|
421
|
+
ruby.to_symbol("reader"),
|
|
422
|
+
RequestReader::new(request_rx.clone(), request_error),
|
|
423
|
+
)?;
|
|
424
|
+
call.aset(
|
|
425
|
+
ruby.to_symbol("sender"),
|
|
426
|
+
StreamSender {
|
|
427
|
+
tx: response_tx.clone(),
|
|
428
|
+
},
|
|
429
|
+
)?;
|
|
430
|
+
if let Some(ms) = timeout_ms {
|
|
431
|
+
call.aset(ruby.to_symbol("timeout_ms"), ms as u64)?;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
let callback = ruby.get_inner(callback);
|
|
435
|
+
let result = callback.funcall::<_, _, Value>("call", (ruby.str_new(path), md, call));
|
|
436
|
+
// Unread request messages are no longer wanted: closing the channel
|
|
437
|
+
// ends the body-reading task now instead of whenever Ruby's GC frees
|
|
438
|
+
// the reader.
|
|
439
|
+
drop(request_rx.lock().take());
|
|
440
|
+
let result = result?;
|
|
441
|
+
|
|
442
|
+
// A String return is a unary response; anything else means the
|
|
443
|
+
// handler already streamed its messages through the sender.
|
|
444
|
+
if let Some(bytes) = RString::from_value(result) {
|
|
445
|
+
let data = unsafe { bytes.as_slice() }.to_vec();
|
|
446
|
+
send_with_interrupts(ruby, response_tx, StreamMessage::Data(data))?;
|
|
447
|
+
}
|
|
448
|
+
Ok(())
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/// Map a Ruby exception to a gRPC status. Only exceptions carrying an
|
|
452
|
+
/// Integer `@code` (i.e. `Tonic::Rails::RpcError`) expose their message;
|
|
453
|
+
/// anything else becomes a generic INTERNAL so details never leak.
|
|
454
|
+
fn error_status(e: &Error) -> (u32, String) {
|
|
455
|
+
if let Some(exc) = e.value() {
|
|
456
|
+
let code = exc
|
|
457
|
+
.funcall::<_, _, Option<i64>>("instance_variable_get", ("@code",))
|
|
458
|
+
.ok()
|
|
459
|
+
.flatten();
|
|
460
|
+
if let Some(code) = code.filter(|c| (1..=16).contains(c)) {
|
|
461
|
+
let message = exc.funcall::<_, _, String>("message", ()).unwrap_or_default();
|
|
462
|
+
return (code as u32, message);
|
|
463
|
+
}
|
|
464
|
+
let description = exc
|
|
465
|
+
.funcall::<_, _, String>("inspect", ())
|
|
466
|
+
.unwrap_or_else(|_| "unknown exception".into());
|
|
467
|
+
eprintln!(
|
|
468
|
+
"tonic-rails: unhandled exception in dispatch: {}",
|
|
469
|
+
crate::framing::sanitize_for_log(&description)
|
|
470
|
+
);
|
|
471
|
+
}
|
|
472
|
+
(tonic::Code::Internal as u32, "internal error".into())
|
|
473
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
//! tonic-rails native extension.
|
|
2
|
+
//!
|
|
3
|
+
//! Serves gRPC with tonic and dispatches each RPC to a Ruby callback.
|
|
4
|
+
//!
|
|
5
|
+
//! ```text
|
|
6
|
+
//! tokio runtime (no GVL) Ruby dispatch threads
|
|
7
|
+
//! ────────────────────── ─────────────────────
|
|
8
|
+
//! tonic transport: HTTP/2, TLS, serve_worker (GVL released
|
|
9
|
+
//! keepalive, health, reflection while idle)
|
|
10
|
+
//! │ ▲
|
|
11
|
+
//! DispatchService (fallback route) │ crossbeam queue
|
|
12
|
+
//! validate → queue DispatchContext ───────────┘
|
|
13
|
+
//! spawn body reader ── request msgs ──▶ RequestReader#next
|
|
14
|
+
//! ResponseBody ◀────── response msgs ── StreamSender#push / return
|
|
15
|
+
//! ```
|
|
16
|
+
//!
|
|
17
|
+
//! Message bytes pass through untouched: Ruby's google-protobuf does all
|
|
18
|
+
//! (de)serialization, so no service definitions are compiled in here.
|
|
19
|
+
//!
|
|
20
|
+
//! Modules:
|
|
21
|
+
//! - [`framing`] — gRPC wire format (frames, gzip, timeouts, trailers)
|
|
22
|
+
//! - [`body`] — response bodies and request stats
|
|
23
|
+
//! - [`dispatch`] — the fallback tower service that queues RPCs for Ruby
|
|
24
|
+
//! - [`gvl`] — GVL release/acquire and the Ruby stream objects
|
|
25
|
+
//! - [`net`] — listener and connection cap
|
|
26
|
+
//! - [`server`] — `TonicRails::Native::Server`
|
|
27
|
+
|
|
28
|
+
mod body;
|
|
29
|
+
mod dispatch;
|
|
30
|
+
mod framing;
|
|
31
|
+
mod gvl;
|
|
32
|
+
mod net;
|
|
33
|
+
mod server;
|
|
34
|
+
|
|
35
|
+
use magnus::{Error, Ruby, function, method, prelude::*};
|
|
36
|
+
|
|
37
|
+
use gvl::{RequestReader, StreamSender};
|
|
38
|
+
use server::NativeServer;
|
|
39
|
+
|
|
40
|
+
#[magnus::init]
|
|
41
|
+
fn init(ruby: &Ruby) -> Result<(), Error> {
|
|
42
|
+
let native = ruby.define_module("TonicRails")?.define_module("Native")?;
|
|
43
|
+
|
|
44
|
+
let class = native.define_class("Server", ruby.class_object())?;
|
|
45
|
+
class.define_singleton_method("new", function!(NativeServer::new, 2))?;
|
|
46
|
+
class.define_method("on_request", method!(NativeServer::on_request, 1))?;
|
|
47
|
+
class.define_method("set_tls", method!(NativeServer::set_tls, 2))?;
|
|
48
|
+
class.define_method("set_tls_client_ca", method!(NativeServer::set_tls_client_ca, 1))?;
|
|
49
|
+
class.define_method(
|
|
50
|
+
"set_max_connections",
|
|
51
|
+
method!(NativeServer::set_max_connections, 1),
|
|
52
|
+
)?;
|
|
53
|
+
class.define_method(
|
|
54
|
+
"set_concurrency_limit_per_connection",
|
|
55
|
+
method!(NativeServer::set_concurrency_limit_per_connection, 1),
|
|
56
|
+
)?;
|
|
57
|
+
class.define_method("set_worker_threads", method!(NativeServer::set_worker_threads, 1))?;
|
|
58
|
+
class.define_method("set_keepalive", method!(NativeServer::set_keepalive, 2))?;
|
|
59
|
+
class.define_method(
|
|
60
|
+
"set_dispatch_queue_size",
|
|
61
|
+
method!(NativeServer::set_dispatch_queue_size, 1),
|
|
62
|
+
)?;
|
|
63
|
+
class.define_method("set_log_requests", method!(NativeServer::set_log_requests, 1))?;
|
|
64
|
+
class.define_method("set_compression", method!(NativeServer::set_compression, 1))?;
|
|
65
|
+
class.define_method("set_reflection", method!(NativeServer::set_reflection, 1))?;
|
|
66
|
+
class.define_method("set_routes", method!(NativeServer::set_routes, 1))?;
|
|
67
|
+
class.define_method(
|
|
68
|
+
"set_reflection_descriptors",
|
|
69
|
+
method!(NativeServer::set_reflection_descriptors, 1),
|
|
70
|
+
)?;
|
|
71
|
+
class.define_method("start_server", method!(NativeServer::start_server, 0))?;
|
|
72
|
+
class.define_method("serve_worker", method!(NativeServer::serve_worker, 0))?;
|
|
73
|
+
class.define_method("begin_shutdown", method!(NativeServer::begin_shutdown, 0))?;
|
|
74
|
+
class.define_method("wait_for_drain", method!(NativeServer::wait_for_drain, 1))?;
|
|
75
|
+
class.define_method("stop_workers", method!(NativeServer::stop_workers, 0))?;
|
|
76
|
+
class.define_method("stop_server", method!(NativeServer::stop_server, 0))?;
|
|
77
|
+
class.define_method("running?", method!(NativeServer::is_running, 0))?;
|
|
78
|
+
class.define_method("stats", method!(NativeServer::stats, 0))?;
|
|
79
|
+
|
|
80
|
+
let sender = native.define_class("StreamSender", ruby.class_object())?;
|
|
81
|
+
sender.undef_default_alloc_func();
|
|
82
|
+
sender.define_method("push", method!(StreamSender::push, 1))?;
|
|
83
|
+
|
|
84
|
+
let reader = native.define_class("RequestReader", ruby.class_object())?;
|
|
85
|
+
reader.undef_default_alloc_func();
|
|
86
|
+
reader.define_method("next", method!(RequestReader::next, 0))?;
|
|
87
|
+
reader.define_method("error", method!(RequestReader::error, 0))?;
|
|
88
|
+
|
|
89
|
+
Ok(())
|
|
90
|
+
}
|