tonic-rails 0.1.0 → 0.3.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 +72 -2
- data/Cargo.lock +5 -5
- data/README.md +204 -13
- data/ext/tonic_rails_native/Cargo.toml +2 -2
- data/ext/tonic_rails_native/src/body.rs +113 -49
- data/ext/tonic_rails_native/src/dispatch.rs +67 -8
- data/ext/tonic_rails_native/src/framing.rs +50 -0
- data/ext/tonic_rails_native/src/gvl.rs +109 -21
- data/ext/tonic_rails_native/src/lib.rs +6 -1
- data/ext/tonic_rails_native/src/log.rs +131 -0
- data/ext/tonic_rails_native/src/net.rs +7 -1
- data/ext/tonic_rails_native/src/server.rs +67 -6
- data/lib/generators/tonic_rails/install/install_generator.rb +45 -0
- data/lib/generators/tonic_rails/install/templates/tonic_rails.rb.tt +21 -0
- data/lib/generators/tonic_rails/service/service_generator.rb +110 -0
- data/lib/generators/tonic_rails/service/templates/handler.rb.tt +38 -0
- data/lib/generators/tonic_rails/service/templates/service.proto.tt +16 -0
- data/lib/generators/tonic_rails/service/templates/service_spec.rb.tt +17 -0
- data/lib/generators/tonic_rails/service/templates/service_test.rb.tt +14 -0
- data/lib/puma/plugin/tonic_rails.rb +116 -0
- data/lib/tonic/rails/base.rb +169 -23
- data/lib/tonic/rails/configuration.rb +27 -4
- data/lib/tonic/rails/health.rb +105 -0
- data/lib/tonic/rails/instrumentation.rb +78 -0
- data/lib/tonic/rails/metadata.rb +50 -0
- data/lib/tonic/rails/peer.rb +60 -0
- data/lib/tonic/rails/railtie.rb +2 -2
- data/lib/tonic/rails/router.rb +1 -0
- data/lib/tonic/rails/rpc_error.rb +73 -6
- data/lib/tonic/rails/server.rb +82 -12
- data/lib/tonic/rails/tasks/tonic-rails.rake +13 -1
- data/lib/tonic/rails/testing/errors.rb +47 -0
- data/lib/tonic/rails/testing/minitest.rb +44 -0
- data/lib/tonic/rails/testing/rspec.rb +53 -0
- data/lib/tonic/rails/testing.rb +35 -4
- data/lib/tonic/rails/version.rb +1 -1
- data/lib/tonic-rails.rb +4 -0
- metadata +17 -1
|
@@ -15,7 +15,11 @@ use std::sync::Arc;
|
|
|
15
15
|
use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
|
|
16
16
|
use std::task::{Context, Poll, Wake, Waker};
|
|
17
17
|
|
|
18
|
-
use
|
|
18
|
+
use crate::log::{Level, LogSink};
|
|
19
|
+
use magnus::{
|
|
20
|
+
Error, RHash, RString, Ruby, Value, prelude::*, r_hash::ForEach, typed_data::DataTypeFunctions,
|
|
21
|
+
value::Opaque,
|
|
22
|
+
};
|
|
19
23
|
use parking_lot::Mutex;
|
|
20
24
|
use rb_sys::{RB_NOGVL_INTR_FAIL, rb_nogvl, rb_thread_call_with_gvl};
|
|
21
25
|
use tokio::sync::{Notify, mpsc};
|
|
@@ -193,35 +197,91 @@ pub fn send_with_interrupts<T>(ruby: &Ruby, tx: &mpsc::Sender<T>, mut msg: T) ->
|
|
|
193
197
|
|
|
194
198
|
// ─── Response stream ──────────────────────────────────────────────
|
|
195
199
|
|
|
200
|
+
/// gRPC metadata as (lower-case name, value) pairs; `-bin` values are
|
|
201
|
+
/// already base64-encoded.
|
|
202
|
+
pub type Metadata = Vec<(String, String)>;
|
|
203
|
+
|
|
196
204
|
/// Messages from the Ruby handler to the tokio response body.
|
|
197
205
|
pub enum StreamMessage {
|
|
206
|
+
/// Response headers (initial metadata). Only ever the first message.
|
|
207
|
+
Headers(Metadata),
|
|
198
208
|
/// One encoded protobuf response message.
|
|
199
209
|
Data(Vec<u8>),
|
|
200
|
-
/// End of stream: `
|
|
201
|
-
|
|
210
|
+
/// End of stream: `status` None = OK, `Some((code, message))` = error,
|
|
211
|
+
/// plus trailing metadata.
|
|
212
|
+
End {
|
|
213
|
+
status: Option<(u32, String)>,
|
|
214
|
+
trailers: Metadata,
|
|
215
|
+
},
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
impl StreamMessage {
|
|
219
|
+
pub fn error(code: tonic::Code, message: &str) -> Self {
|
|
220
|
+
StreamMessage::End {
|
|
221
|
+
status: Some((code as u32, message.to_owned())),
|
|
222
|
+
trailers: Vec::new(),
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/// Convert a Ruby Hash of String => String into metadata pairs.
|
|
228
|
+
fn metadata_from_hash(hash: RHash) -> Result<Metadata, Error> {
|
|
229
|
+
let mut pairs = Vec::with_capacity(hash.len());
|
|
230
|
+
hash.foreach(|name: String, value: String| {
|
|
231
|
+
pairs.push((name, value));
|
|
232
|
+
Ok(ForEach::Continue)
|
|
233
|
+
})?;
|
|
234
|
+
Ok(pairs)
|
|
202
235
|
}
|
|
203
236
|
|
|
204
237
|
/// `TonicRails::Native::StreamSender` — pushes encoded response messages
|
|
205
|
-
/// to the client with backpressure.
|
|
238
|
+
/// (and response metadata) to the client with backpressure.
|
|
206
239
|
#[derive(magnus::TypedData)]
|
|
207
240
|
#[magnus(class = "TonicRails::Native::StreamSender", free_immediately)]
|
|
208
241
|
pub struct StreamSender {
|
|
209
242
|
tx: mpsc::Sender<StreamMessage>,
|
|
243
|
+
trailers: Arc<Mutex<Metadata>>,
|
|
244
|
+
started: AtomicBool,
|
|
210
245
|
}
|
|
211
246
|
|
|
212
247
|
impl StreamSender {
|
|
248
|
+
fn closed(ruby: &Ruby) -> Error {
|
|
249
|
+
Error::new(ruby.exception_io_error(), "response stream closed by peer")
|
|
250
|
+
}
|
|
251
|
+
|
|
213
252
|
/// Raises `IOError` if the client has gone away.
|
|
214
253
|
pub fn push(ruby: &Ruby, this: &Self, data: RString) -> Result<(), Error> {
|
|
254
|
+
this.started.store(true, Ordering::Release);
|
|
215
255
|
let bytes = unsafe { data.as_slice() }.to_vec();
|
|
216
256
|
if send_with_interrupts(ruby, &this.tx, StreamMessage::Data(bytes))? {
|
|
217
257
|
Ok(())
|
|
218
258
|
} else {
|
|
219
|
-
Err(
|
|
220
|
-
ruby.exception_io_error(),
|
|
221
|
-
"response stream closed by peer",
|
|
222
|
-
))
|
|
259
|
+
Err(Self::closed(ruby))
|
|
223
260
|
}
|
|
224
261
|
}
|
|
262
|
+
|
|
263
|
+
/// Send response headers. Must come before any message.
|
|
264
|
+
pub fn send_headers(ruby: &Ruby, this: &Self, headers: RHash) -> Result<(), Error> {
|
|
265
|
+
if this.started.swap(true, Ordering::AcqRel) {
|
|
266
|
+
return Err(Error::new(
|
|
267
|
+
ruby.exception_runtime_error(),
|
|
268
|
+
"response headers must be sent before any response message",
|
|
269
|
+
));
|
|
270
|
+
}
|
|
271
|
+
let headers = metadata_from_hash(headers)?;
|
|
272
|
+
if send_with_interrupts(ruby, &this.tx, StreamMessage::Headers(headers))? {
|
|
273
|
+
Ok(())
|
|
274
|
+
} else {
|
|
275
|
+
Err(Self::closed(ruby))
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/// Add trailing metadata, sent with the final status.
|
|
280
|
+
pub fn add_trailers(&self, trailers: RHash) -> Result<(), Error> {
|
|
281
|
+
let trailers = metadata_from_hash(trailers)?;
|
|
282
|
+
self.trailers.lock().extend(trailers);
|
|
283
|
+
Ok(())
|
|
284
|
+
}
|
|
225
285
|
}
|
|
226
286
|
|
|
227
287
|
impl DataTypeFunctions for StreamSender {}
|
|
@@ -308,6 +368,16 @@ pub struct DispatchContext {
|
|
|
308
368
|
pub response_tx: mpsc::Sender<StreamMessage>,
|
|
309
369
|
pub request_rx: mpsc::Receiver<Vec<u8>>,
|
|
310
370
|
pub request_error: Arc<RequestError>,
|
|
371
|
+
pub log: LogSink,
|
|
372
|
+
pub peer: Peer,
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/// The client connection: remote address and, with mutual TLS, its
|
|
376
|
+
/// certificate chain (DER, leaf first).
|
|
377
|
+
#[derive(Default)]
|
|
378
|
+
pub struct Peer {
|
|
379
|
+
pub address: Option<String>,
|
|
380
|
+
pub certificates: Vec<Vec<u8>>,
|
|
311
381
|
}
|
|
312
382
|
|
|
313
383
|
/// Bounds on how long one thread keeps the GVL to drain a busy queue.
|
|
@@ -357,17 +427,15 @@ pub fn dispatch(first: Box<DispatchContext>, queue: &crossbeam_channel::Receiver
|
|
|
357
427
|
|
|
358
428
|
fn run_guarded(ctx: DispatchContext) {
|
|
359
429
|
let response_tx = ctx.response_tx.clone();
|
|
430
|
+
let log = ctx.log.clone();
|
|
360
431
|
if let Err(panic) = catch_unwind(AssertUnwindSafe(|| run_handler(ctx))) {
|
|
361
432
|
let msg = panic
|
|
362
433
|
.downcast_ref::<String>()
|
|
363
434
|
.map(String::as_str)
|
|
364
435
|
.or_else(|| panic.downcast_ref::<&str>().copied())
|
|
365
436
|
.unwrap_or("unknown panic");
|
|
366
|
-
|
|
367
|
-
let _ = response_tx.try_send(StreamMessage::
|
|
368
|
-
tonic::Code::Internal as u32,
|
|
369
|
-
"internal error".into(),
|
|
370
|
-
))));
|
|
437
|
+
log.log(Level::Error, &format!("panic in dispatch: {msg}"));
|
|
438
|
+
let _ = response_tx.try_send(StreamMessage::error(tonic::Code::Internal, "internal error"));
|
|
371
439
|
}
|
|
372
440
|
}
|
|
373
441
|
|
|
@@ -381,9 +449,12 @@ fn run_handler(ctx: DispatchContext) {
|
|
|
381
449
|
response_tx,
|
|
382
450
|
request_rx,
|
|
383
451
|
request_error,
|
|
452
|
+
log,
|
|
453
|
+
peer,
|
|
384
454
|
} = ctx;
|
|
385
455
|
|
|
386
|
-
let
|
|
456
|
+
let trailers = Arc::new(Mutex::new(Metadata::new()));
|
|
457
|
+
let result = call_handler(
|
|
387
458
|
&ruby,
|
|
388
459
|
callback,
|
|
389
460
|
&path,
|
|
@@ -392,9 +463,12 @@ fn run_handler(ctx: DispatchContext) {
|
|
|
392
463
|
&response_tx,
|
|
393
464
|
request_rx,
|
|
394
465
|
request_error,
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
466
|
+
trailers.clone(),
|
|
467
|
+
&peer,
|
|
468
|
+
);
|
|
469
|
+
let end = StreamMessage::End {
|
|
470
|
+
status: result.err().map(|e| error_status(&e, &log)),
|
|
471
|
+
trailers: std::mem::take(&mut *trailers.lock()),
|
|
398
472
|
};
|
|
399
473
|
// Ignore failures: the client may already be gone.
|
|
400
474
|
let _ = send_with_interrupts(&ruby, &response_tx, end);
|
|
@@ -410,6 +484,8 @@ fn call_handler(
|
|
|
410
484
|
response_tx: &mpsc::Sender<StreamMessage>,
|
|
411
485
|
request_rx: mpsc::Receiver<Vec<u8>>,
|
|
412
486
|
request_error: Arc<RequestError>,
|
|
487
|
+
trailers: Arc<Mutex<Metadata>>,
|
|
488
|
+
peer: &Peer,
|
|
413
489
|
) -> Result<(), Error> {
|
|
414
490
|
let md = ruby.hash_new_capa(metadata.len());
|
|
415
491
|
for (k, v) in metadata {
|
|
@@ -425,11 +501,23 @@ fn call_handler(
|
|
|
425
501
|
ruby.to_symbol("sender"),
|
|
426
502
|
StreamSender {
|
|
427
503
|
tx: response_tx.clone(),
|
|
504
|
+
trailers,
|
|
505
|
+
started: AtomicBool::new(false),
|
|
428
506
|
},
|
|
429
507
|
)?;
|
|
430
508
|
if let Some(ms) = timeout_ms {
|
|
431
509
|
call.aset(ruby.to_symbol("timeout_ms"), ms as u64)?;
|
|
432
510
|
}
|
|
511
|
+
if let Some(address) = &peer.address {
|
|
512
|
+
call.aset(ruby.to_symbol("peer_address"), ruby.str_new(address))?;
|
|
513
|
+
}
|
|
514
|
+
if !peer.certificates.is_empty() {
|
|
515
|
+
let certificates = ruby.ary_new_capa(peer.certificates.len());
|
|
516
|
+
for der in &peer.certificates {
|
|
517
|
+
certificates.push(ruby.str_from_slice(der))?;
|
|
518
|
+
}
|
|
519
|
+
call.aset(ruby.to_symbol("peer_certificates"), certificates)?;
|
|
520
|
+
}
|
|
433
521
|
|
|
434
522
|
let callback = ruby.get_inner(callback);
|
|
435
523
|
let result = callback.funcall::<_, _, Value>("call", (ruby.str_new(path), md, call));
|
|
@@ -451,7 +539,7 @@ fn call_handler(
|
|
|
451
539
|
/// Map a Ruby exception to a gRPC status. Only exceptions carrying an
|
|
452
540
|
/// Integer `@code` (i.e. `Tonic::Rails::RpcError`) expose their message;
|
|
453
541
|
/// anything else becomes a generic INTERNAL so details never leak.
|
|
454
|
-
fn error_status(e: &Error) -> (u32, String) {
|
|
542
|
+
fn error_status(e: &Error, log: &LogSink) -> (u32, String) {
|
|
455
543
|
if let Some(exc) = e.value() {
|
|
456
544
|
let code = exc
|
|
457
545
|
.funcall::<_, _, Option<i64>>("instance_variable_get", ("@code",))
|
|
@@ -464,9 +552,9 @@ fn error_status(e: &Error) -> (u32, String) {
|
|
|
464
552
|
let description = exc
|
|
465
553
|
.funcall::<_, _, String>("inspect", ())
|
|
466
554
|
.unwrap_or_else(|_| "unknown exception".into());
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
555
|
+
log.log(
|
|
556
|
+
Level::Error,
|
|
557
|
+
&format!("unhandled exception in dispatch: {description}"),
|
|
470
558
|
);
|
|
471
559
|
}
|
|
472
560
|
(tonic::Code::Internal as u32, "internal error".into())
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
//! - [`body`] — response bodies and request stats
|
|
23
23
|
//! - [`dispatch`] — the fallback tower service that queues RPCs for Ruby
|
|
24
24
|
//! - [`gvl`] — GVL release/acquire and the Ruby stream objects
|
|
25
|
+
//! - [`log`] — transport log events for Ruby's logger
|
|
25
26
|
//! - [`net`] — listener and connection cap
|
|
26
27
|
//! - [`server`] — `TonicRails::Native::Server`
|
|
27
28
|
|
|
@@ -29,6 +30,7 @@ mod body;
|
|
|
29
30
|
mod dispatch;
|
|
30
31
|
mod framing;
|
|
31
32
|
mod gvl;
|
|
33
|
+
mod log;
|
|
32
34
|
mod net;
|
|
33
35
|
mod server;
|
|
34
36
|
|
|
@@ -60,7 +62,6 @@ fn init(ruby: &Ruby) -> Result<(), Error> {
|
|
|
60
62
|
"set_dispatch_queue_size",
|
|
61
63
|
method!(NativeServer::set_dispatch_queue_size, 1),
|
|
62
64
|
)?;
|
|
63
|
-
class.define_method("set_log_requests", method!(NativeServer::set_log_requests, 1))?;
|
|
64
65
|
class.define_method("set_compression", method!(NativeServer::set_compression, 1))?;
|
|
65
66
|
class.define_method("set_reflection", method!(NativeServer::set_reflection, 1))?;
|
|
66
67
|
class.define_method("set_routes", method!(NativeServer::set_routes, 1))?;
|
|
@@ -70,16 +71,20 @@ fn init(ruby: &Ruby) -> Result<(), Error> {
|
|
|
70
71
|
)?;
|
|
71
72
|
class.define_method("start_server", method!(NativeServer::start_server, 0))?;
|
|
72
73
|
class.define_method("serve_worker", method!(NativeServer::serve_worker, 0))?;
|
|
74
|
+
class.define_method("set_health", method!(NativeServer::set_health, 2))?;
|
|
73
75
|
class.define_method("begin_shutdown", method!(NativeServer::begin_shutdown, 0))?;
|
|
74
76
|
class.define_method("wait_for_drain", method!(NativeServer::wait_for_drain, 1))?;
|
|
75
77
|
class.define_method("stop_workers", method!(NativeServer::stop_workers, 0))?;
|
|
76
78
|
class.define_method("stop_server", method!(NativeServer::stop_server, 0))?;
|
|
79
|
+
class.define_method("next_log_events", method!(NativeServer::next_log_events, 1))?;
|
|
77
80
|
class.define_method("running?", method!(NativeServer::is_running, 0))?;
|
|
78
81
|
class.define_method("stats", method!(NativeServer::stats, 0))?;
|
|
79
82
|
|
|
80
83
|
let sender = native.define_class("StreamSender", ruby.class_object())?;
|
|
81
84
|
sender.undef_default_alloc_func();
|
|
82
85
|
sender.define_method("push", method!(StreamSender::push, 1))?;
|
|
86
|
+
sender.define_method("send_headers", method!(StreamSender::send_headers, 1))?;
|
|
87
|
+
sender.define_method("add_trailers", method!(StreamSender::add_trailers, 1))?;
|
|
83
88
|
|
|
84
89
|
let reader = native.define_class("RequestReader", ruby.class_object())?;
|
|
85
90
|
reader.undef_default_alloc_func();
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
//! Transport log events, delivered to Ruby's logger.
|
|
2
|
+
//!
|
|
3
|
+
//! tokio threads cannot call Ruby, so events go through a bounded channel
|
|
4
|
+
//! that a Ruby thread drains (`Server#next_log_events`). If nobody is
|
|
5
|
+
//! draining, or the channel is full, events fall back to stderr.
|
|
6
|
+
|
|
7
|
+
use crossbeam_channel::{Receiver, Sender, TrySendError};
|
|
8
|
+
use tonic::Code;
|
|
9
|
+
|
|
10
|
+
use crate::framing::sanitize_for_log;
|
|
11
|
+
|
|
12
|
+
/// Events buffered before falling back to stderr.
|
|
13
|
+
const CAPACITY: usize = 1024;
|
|
14
|
+
|
|
15
|
+
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
16
|
+
pub enum Level {
|
|
17
|
+
Info,
|
|
18
|
+
Warn,
|
|
19
|
+
Error,
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
impl Level {
|
|
23
|
+
/// Client-caused outcomes are informational; overload is a warning;
|
|
24
|
+
/// anything that suggests a server fault is an error.
|
|
25
|
+
pub fn for_code(code: u32) -> Level {
|
|
26
|
+
match Code::from_i32(code as i32) {
|
|
27
|
+
Code::Internal | Code::Unknown | Code::DataLoss => Level::Error,
|
|
28
|
+
Code::ResourceExhausted | Code::Unavailable => Level::Warn,
|
|
29
|
+
_ => Level::Info,
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
pub fn as_str(self) -> &'static str {
|
|
34
|
+
match self {
|
|
35
|
+
Level::Info => "info",
|
|
36
|
+
Level::Warn => "warn",
|
|
37
|
+
Level::Error => "error",
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const STATUS_NAMES: [&str; 17] = [
|
|
43
|
+
"OK",
|
|
44
|
+
"CANCELLED",
|
|
45
|
+
"UNKNOWN",
|
|
46
|
+
"INVALID_ARGUMENT",
|
|
47
|
+
"DEADLINE_EXCEEDED",
|
|
48
|
+
"NOT_FOUND",
|
|
49
|
+
"ALREADY_EXISTS",
|
|
50
|
+
"PERMISSION_DENIED",
|
|
51
|
+
"RESOURCE_EXHAUSTED",
|
|
52
|
+
"FAILED_PRECONDITION",
|
|
53
|
+
"ABORTED",
|
|
54
|
+
"OUT_OF_RANGE",
|
|
55
|
+
"UNIMPLEMENTED",
|
|
56
|
+
"INTERNAL",
|
|
57
|
+
"UNAVAILABLE",
|
|
58
|
+
"DATA_LOSS",
|
|
59
|
+
"UNAUTHENTICATED",
|
|
60
|
+
];
|
|
61
|
+
|
|
62
|
+
/// The canonical gRPC status name, e.g. `NOT_FOUND`.
|
|
63
|
+
pub fn status_name(code: u32) -> &'static str {
|
|
64
|
+
STATUS_NAMES.get(code as usize).copied().unwrap_or("UNKNOWN")
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
pub struct LogEvent {
|
|
68
|
+
pub level: Level,
|
|
69
|
+
pub message: String,
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
#[derive(Clone)]
|
|
73
|
+
pub struct LogSink {
|
|
74
|
+
tx: Sender<LogEvent>,
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
impl LogSink {
|
|
78
|
+
pub fn new() -> (Self, Receiver<LogEvent>) {
|
|
79
|
+
let (tx, rx) = crossbeam_channel::bounded(CAPACITY);
|
|
80
|
+
(Self { tx }, rx)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
pub fn log(&self, level: Level, message: &str) {
|
|
84
|
+
let event = LogEvent {
|
|
85
|
+
level,
|
|
86
|
+
message: sanitize_for_log(message),
|
|
87
|
+
};
|
|
88
|
+
match self.tx.try_send(event) {
|
|
89
|
+
Ok(()) => {}
|
|
90
|
+
Err(TrySendError::Full(event) | TrySendError::Disconnected(event)) => {
|
|
91
|
+
eprintln!("tonic-rails: [{}] {}", event.level.as_str(), event.message);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
impl Default for LogSink {
|
|
98
|
+
/// A sink nobody drains: every event goes to stderr.
|
|
99
|
+
fn default() -> Self {
|
|
100
|
+
Self::new().0
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
#[cfg(test)]
|
|
105
|
+
mod tests {
|
|
106
|
+
use super::*;
|
|
107
|
+
|
|
108
|
+
#[test]
|
|
109
|
+
fn levels_by_status() {
|
|
110
|
+
assert_eq!(Level::for_code(Code::Internal as u32), Level::Error);
|
|
111
|
+
assert_eq!(Level::for_code(Code::ResourceExhausted as u32), Level::Warn);
|
|
112
|
+
assert_eq!(Level::for_code(Code::Unimplemented as u32), Level::Info);
|
|
113
|
+
assert_eq!(Level::for_code(Code::DeadlineExceeded as u32), Level::Info);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
#[test]
|
|
117
|
+
fn status_names() {
|
|
118
|
+
assert_eq!(status_name(0), "OK");
|
|
119
|
+
assert_eq!(status_name(12), "UNIMPLEMENTED");
|
|
120
|
+
assert_eq!(status_name(99), "UNKNOWN");
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
#[test]
|
|
124
|
+
fn events_reach_the_receiver_sanitized() {
|
|
125
|
+
let (sink, rx) = LogSink::new();
|
|
126
|
+
sink.log(Level::Warn, "queue full\nforged line");
|
|
127
|
+
let event = rx.try_recv().expect("event");
|
|
128
|
+
assert_eq!(event.level, Level::Warn);
|
|
129
|
+
assert_eq!(event.message, "queue full forged line");
|
|
130
|
+
}
|
|
131
|
+
}
|
|
@@ -14,6 +14,8 @@ use tokio::sync::{OwnedSemaphorePermit, Semaphore};
|
|
|
14
14
|
use tokio_stream::Stream;
|
|
15
15
|
use tonic::transport::server::{Connected, TcpConnectInfo};
|
|
16
16
|
|
|
17
|
+
use crate::log::{Level, LogSink};
|
|
18
|
+
|
|
17
19
|
/// Bind with SO_REUSEADDR + SO_REUSEPORT so several worker processes can
|
|
18
20
|
/// share a port (the kernel load-balances accepts) and restarts don't
|
|
19
21
|
/// fail on TIME_WAIT. Must be called inside a tokio runtime context.
|
|
@@ -110,6 +112,7 @@ pub struct LimitedIncoming<S> {
|
|
|
110
112
|
pub inner: S,
|
|
111
113
|
pub semaphore: Arc<Semaphore>,
|
|
112
114
|
pub active: Arc<AtomicUsize>,
|
|
115
|
+
pub log: LogSink,
|
|
113
116
|
}
|
|
114
117
|
|
|
115
118
|
impl<S> Stream for LimitedIncoming<S>
|
|
@@ -138,7 +141,10 @@ where
|
|
|
138
141
|
}
|
|
139
142
|
Err(_) => {
|
|
140
143
|
let active = self.active.load(Ordering::Relaxed);
|
|
141
|
-
|
|
144
|
+
self.log.log(
|
|
145
|
+
Level::Warn,
|
|
146
|
+
&format!("connection refused: max_connections reached ({active} active)"),
|
|
147
|
+
);
|
|
142
148
|
}
|
|
143
149
|
}
|
|
144
150
|
}
|
|
@@ -32,6 +32,7 @@ use tonic_health::server::HealthReporter;
|
|
|
32
32
|
use crate::body::ServerStats;
|
|
33
33
|
use crate::dispatch::{DispatchInner, DispatchService};
|
|
34
34
|
use crate::gvl::{self, Cancel, DispatchContext};
|
|
35
|
+
use crate::log::{Level, LogEvent, LogSink};
|
|
35
36
|
use crate::net::{LimitedIncoming, bind};
|
|
36
37
|
|
|
37
38
|
/// The overall-server health entry (empty service name).
|
|
@@ -91,6 +92,8 @@ pub struct NativeServer {
|
|
|
91
92
|
stats: Arc<ServerStats>,
|
|
92
93
|
compression: Arc<AtomicBool>,
|
|
93
94
|
active_connections: Arc<AtomicUsize>,
|
|
95
|
+
/// Transport log events waiting for Ruby (`next_log_events`).
|
|
96
|
+
log_rx: crossbeam_channel::Receiver<LogEvent>,
|
|
94
97
|
}
|
|
95
98
|
|
|
96
99
|
impl DataTypeFunctions for NativeServer {
|
|
@@ -128,15 +131,20 @@ impl NativeServer {
|
|
|
128
131
|
bind_address
|
|
129
132
|
.parse::<SocketAddr>()
|
|
130
133
|
.map_err(|e| arg_error(ruby, format!("invalid bind address {bind_address:?}: {e}")))?;
|
|
134
|
+
let (log, log_rx) = LogSink::new();
|
|
131
135
|
Ok(Self {
|
|
132
136
|
bind_address,
|
|
133
137
|
max_message_size,
|
|
134
138
|
callback: Mutex::new(None),
|
|
135
139
|
options: Mutex::new(Options::default()),
|
|
136
140
|
running: Mutex::new(None),
|
|
137
|
-
stats: Arc::new(ServerStats
|
|
141
|
+
stats: Arc::new(ServerStats {
|
|
142
|
+
log,
|
|
143
|
+
..ServerStats::default()
|
|
144
|
+
}),
|
|
138
145
|
compression: Arc::new(AtomicBool::new(false)),
|
|
139
146
|
active_connections: Arc::new(AtomicUsize::new(0)),
|
|
147
|
+
log_rx,
|
|
140
148
|
})
|
|
141
149
|
}
|
|
142
150
|
|
|
@@ -187,10 +195,6 @@ impl NativeServer {
|
|
|
187
195
|
self.options.lock().dispatch_queue_size = size.max(1);
|
|
188
196
|
}
|
|
189
197
|
|
|
190
|
-
pub fn set_log_requests(&self, enabled: bool) {
|
|
191
|
-
self.stats.log_requests.store(enabled, Ordering::Relaxed);
|
|
192
|
-
}
|
|
193
|
-
|
|
194
198
|
pub fn set_compression(&self, enabled: bool) {
|
|
195
199
|
self.compression.store(enabled, Ordering::Relaxed);
|
|
196
200
|
}
|
|
@@ -353,16 +357,18 @@ impl NativeServer {
|
|
|
353
357
|
inner: tokio_stream::wrappers::TcpListenerStream::new(listener),
|
|
354
358
|
semaphore: Arc::new(Semaphore::new(permits)),
|
|
355
359
|
active: this.active_connections.clone(),
|
|
360
|
+
log: this.stats.log.clone(),
|
|
356
361
|
};
|
|
357
362
|
|
|
358
363
|
let (shutdown_tx, mut shutdown_rx) = watch::channel(false);
|
|
359
364
|
let (done_tx, done_rx) = crossbeam_channel::bounded(1);
|
|
365
|
+
let log = this.stats.log.clone();
|
|
360
366
|
runtime.spawn(async move {
|
|
361
367
|
let signal = async move {
|
|
362
368
|
let _ = shutdown_rx.wait_for(|stop| *stop).await;
|
|
363
369
|
};
|
|
364
370
|
if let Err(e) = router.serve_with_incoming_shutdown(incoming, signal).await {
|
|
365
|
-
|
|
371
|
+
log.log(Level::Error, &format!("server error: {e}"));
|
|
366
372
|
}
|
|
367
373
|
let _ = done_tx.send(());
|
|
368
374
|
});
|
|
@@ -403,6 +409,27 @@ impl NativeServer {
|
|
|
403
409
|
Ok(())
|
|
404
410
|
}
|
|
405
411
|
|
|
412
|
+
/// Set the grpc.health.v1 status of one service ("" = the whole
|
|
413
|
+
/// server). Returns false if the server isn't running.
|
|
414
|
+
pub fn set_health(&self, service: String, serving: bool) -> bool {
|
|
415
|
+
let Some(r) = self
|
|
416
|
+
.running
|
|
417
|
+
.lock()
|
|
418
|
+
.as_ref()
|
|
419
|
+
.map(|r| (r.runtime.handle().clone(), r.health.clone()))
|
|
420
|
+
else {
|
|
421
|
+
return false;
|
|
422
|
+
};
|
|
423
|
+
let (handle, health) = r;
|
|
424
|
+
let status = if serving {
|
|
425
|
+
ServingStatus::Serving
|
|
426
|
+
} else {
|
|
427
|
+
ServingStatus::NotServing
|
|
428
|
+
};
|
|
429
|
+
handle.block_on(health.set_service_status(service, status));
|
|
430
|
+
true
|
|
431
|
+
}
|
|
432
|
+
|
|
406
433
|
/// Mark everything NOT_SERVING and stop accepting new connections.
|
|
407
434
|
/// Existing connections receive GOAWAY; in-flight RPCs continue.
|
|
408
435
|
pub fn begin_shutdown(&self) {
|
|
@@ -478,6 +505,40 @@ impl NativeServer {
|
|
|
478
505
|
}
|
|
479
506
|
}
|
|
480
507
|
|
|
508
|
+
/// Wait up to `timeout` seconds (GVL released) for transport log
|
|
509
|
+
/// events and return them as `[[level, message], ...]`, oldest first.
|
|
510
|
+
/// Returns an empty Array on timeout.
|
|
511
|
+
pub fn next_log_events(ruby: &Ruby, this: &Self, timeout: f64) -> Result<RArray, Error> {
|
|
512
|
+
let cancel = Cancel::default();
|
|
513
|
+
let rx = &this.log_rx;
|
|
514
|
+
let deadline = Instant::now() + secs(timeout);
|
|
515
|
+
let first = gvl::without_gvl(&cancel, || {
|
|
516
|
+
while !cancel.is_cancelled() {
|
|
517
|
+
let remaining = deadline.saturating_duration_since(Instant::now());
|
|
518
|
+
if remaining.is_zero() {
|
|
519
|
+
return None;
|
|
520
|
+
}
|
|
521
|
+
match rx.recv_timeout(remaining.min(Duration::from_millis(100))) {
|
|
522
|
+
Ok(event) => return Some(event),
|
|
523
|
+
Err(crossbeam_channel::RecvTimeoutError::Timeout) => {}
|
|
524
|
+
Err(crossbeam_channel::RecvTimeoutError::Disconnected) => return None,
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
None
|
|
528
|
+
})
|
|
529
|
+
.flatten();
|
|
530
|
+
|
|
531
|
+
let events = ruby.ary_new();
|
|
532
|
+
for event in first.into_iter().chain(rx.try_iter().take(255)) {
|
|
533
|
+
events.push((ruby.to_symbol(event.level.as_str()), event.message))?;
|
|
534
|
+
}
|
|
535
|
+
if events.is_empty() {
|
|
536
|
+
// Interrupted: let Ruby run trap handlers / raise before retrying.
|
|
537
|
+
ruby.thread_check_ints()?;
|
|
538
|
+
}
|
|
539
|
+
Ok(events)
|
|
540
|
+
}
|
|
541
|
+
|
|
481
542
|
pub fn is_running(&self) -> bool {
|
|
482
543
|
self.running.lock().is_some()
|
|
483
544
|
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators"
|
|
4
|
+
|
|
5
|
+
module TonicRails
|
|
6
|
+
module Generators
|
|
7
|
+
# rails generate tonic_rails:install
|
|
8
|
+
#
|
|
9
|
+
# Sets an app up for gRPC: an initializer (load path for generated
|
|
10
|
+
# protobuf code, configuration, handler registration), the proto and
|
|
11
|
+
# generated-code directories, and the Puma plugin.
|
|
12
|
+
class InstallGenerator < ::Rails::Generators::Base
|
|
13
|
+
source_root File.expand_path("templates", __dir__)
|
|
14
|
+
|
|
15
|
+
class_option :skip_puma, type: :boolean, default: false,
|
|
16
|
+
desc: "Don't add `plugin :tonic_rails` to config/puma.rb"
|
|
17
|
+
class_option :skip_next_steps, type: :boolean, default: false, hide: true
|
|
18
|
+
|
|
19
|
+
def create_initializer
|
|
20
|
+
template "tonic_rails.rb.tt", "config/initializers/tonic_rails.rb"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def create_directories
|
|
24
|
+
create_file "app/protos/.keep"
|
|
25
|
+
create_file "lib/grpc/.keep"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def add_puma_plugin
|
|
29
|
+
return if options[:skip_puma]
|
|
30
|
+
|
|
31
|
+
puma = "config/puma.rb"
|
|
32
|
+
return unless File.exist?(File.join(destination_root, puma))
|
|
33
|
+
return if File.read(File.join(destination_root, puma)).match?(/plugin\s+:tonic_rails/)
|
|
34
|
+
|
|
35
|
+
append_to_file puma, "\n# Run the tonic-rails gRPC server alongside Puma.\nplugin :tonic_rails\n"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def show_next_steps
|
|
39
|
+
return if options[:skip_next_steps]
|
|
40
|
+
|
|
41
|
+
say "\nNext: rails generate tonic_rails:service Item GetItem ListItems:server", :green
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Ruby code generated from app/protos by `bin/rails tonic_rails:compile_protos`
|
|
4
|
+
# lives in lib/grpc. Its files require each other by plain name, so put the
|
|
5
|
+
# directory on the load path and keep Zeitwerk away from it.
|
|
6
|
+
Rails.root.join("lib/grpc").then do |generated|
|
|
7
|
+
$LOAD_PATH.unshift(generated.to_s) unless $LOAD_PATH.include?(generated.to_s)
|
|
8
|
+
Rails.autoloaders.main.ignore(generated)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
Tonic::Rails.configure do |config|
|
|
12
|
+
# config.bind_address = "0.0.0.0:50051"
|
|
13
|
+
# config.dispatch_threads = ENV.fetch("RAILS_MAX_THREADS", 5).to_i
|
|
14
|
+
# config.reflection = Rails.env.development?
|
|
15
|
+
# config.health_check = -> { ActiveRecord::Base.connection.active? }
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Handlers in app/grpc are autoloaded; register them here so they're
|
|
19
|
+
# picked up again after code reloads.
|
|
20
|
+
Rails.application.config.to_prepare do
|
|
21
|
+
end
|