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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8b39b4d814c854c16c5a1f9c7ec05415402fdbb49716846bc3883442931f5165
|
|
4
|
+
data.tar.gz: f4dcedf8d10ec7cd0786232a7637e192ec5dd7746ee15542322465d56bc11436
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b1b5406e16afb900514ae85ae11350d7a7546c0a08ed27c3c1042622de85b4ea9ac692185e3a0d27792f95ca36e61a1f2ff0b5729bf8e38eb645ed9e45c011ee
|
|
7
|
+
data.tar.gz: 7b3b7386f1c8342cf0c25fce33ea1564c38e1be027788b7b919377354b9a9a53f3c58338e334185d8dd0377f261b9775fadeee1cc4a2b0ac627764737329fd2b
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,47 @@ minor versions may contain breaking changes.
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.6.0] - 2026-10-11
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- Client: `Tonic::Rails::Client` makes unary calls to other gRPC
|
|
15
|
+
services without the grpc gem. Stubs come from the descriptors that
|
|
16
|
+
`tonic_rails:compile_protos` generates. Calls take deadlines (and
|
|
17
|
+
default to the deadline of the RPC being handled), send metadata,
|
|
18
|
+
return headers and trailers with `return_call: true`, and raise
|
|
19
|
+
`RpcError` with rich details. TLS uses the system's root certificates,
|
|
20
|
+
a custom CA or mutual TLS. Clients keep working across `fork` with no
|
|
21
|
+
hooks, and `Timeout` and `Thread#raise` interrupt calls. Each call
|
|
22
|
+
publishes a `request_rpc.tonic_rails` event. Closes #37.
|
|
23
|
+
- Client: server-streaming calls. The stub method returns a stream that
|
|
24
|
+
yields each message as it arrives. A slow reader holds the server back
|
|
25
|
+
through HTTP/2 flow control, and stopping early (`break`, `first(n)`,
|
|
26
|
+
an exception) cancels the call. The deadline covers the whole stream,
|
|
27
|
+
an error partway through raises `RpcError` after the messages before
|
|
28
|
+
it, and `headers` and `trailers` are available once the stream ends.
|
|
29
|
+
Closes #38.
|
|
30
|
+
|
|
31
|
+
## [0.5.0] - 2026-10-11
|
|
32
|
+
|
|
33
|
+
### Added
|
|
34
|
+
|
|
35
|
+
- Multi-process mode: `config.processes = 4` runs four server processes
|
|
36
|
+
on one port, so services that spend their time in Ruby use four cores
|
|
37
|
+
instead of one. The app loads once and forks the workers. They share
|
|
38
|
+
the listening socket, with new connections going to the least busy
|
|
39
|
+
worker. Workers that die are replaced, and SIGTERM drains them all.
|
|
40
|
+
`on_worker_boot` runs in each worker after fork. Works with
|
|
41
|
+
`tonic_rails:serve` and the Puma plugin; Unix only. Closes #23.
|
|
42
|
+
- Benchmark: `PROCESSES` and `CONNECTIONS` for `bench/run_bench.sh`, and
|
|
43
|
+
`-connections` for the Go client.
|
|
44
|
+
|
|
45
|
+
### Changed
|
|
46
|
+
|
|
47
|
+
- The Puma plugin waits `drain_timeout` + 10 seconds (was + 5) for the
|
|
48
|
+
gRPC server to stop before killing it, which leaves a multi-process
|
|
49
|
+
server time to stop its own workers.
|
|
50
|
+
|
|
10
51
|
## [0.4.0] - 2026-10-11
|
|
11
52
|
|
|
12
53
|
### Added
|
|
@@ -125,7 +166,9 @@ First public release.
|
|
|
125
166
|
- Precompiled native gems for Linux (x86_64, aarch64; glibc and musl) and
|
|
126
167
|
macOS (x86_64, arm64), Ruby 3.3 to 4.0.
|
|
127
168
|
|
|
128
|
-
[Unreleased]: https://github.com/CodingAnarchy/tonic-rails/compare/v0.
|
|
169
|
+
[Unreleased]: https://github.com/CodingAnarchy/tonic-rails/compare/v0.6.0...HEAD
|
|
170
|
+
[0.6.0]: https://github.com/CodingAnarchy/tonic-rails/compare/v0.5.0...v0.6.0
|
|
171
|
+
[0.5.0]: https://github.com/CodingAnarchy/tonic-rails/compare/v0.4.0...v0.5.0
|
|
129
172
|
[0.4.0]: https://github.com/CodingAnarchy/tonic-rails/compare/v0.3.0...v0.4.0
|
|
130
173
|
[0.3.0]: https://github.com/CodingAnarchy/tonic-rails/compare/v0.2.0...v0.3.0
|
|
131
174
|
[0.2.0]: https://github.com/CodingAnarchy/tonic-rails/compare/v0.1.0...v0.2.0
|
data/Cargo.lock
CHANGED
|
@@ -155,6 +155,22 @@ dependencies = [
|
|
|
155
155
|
"libloading",
|
|
156
156
|
]
|
|
157
157
|
|
|
158
|
+
[[package]]
|
|
159
|
+
name = "core-foundation"
|
|
160
|
+
version = "0.10.1"
|
|
161
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
162
|
+
checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6"
|
|
163
|
+
dependencies = [
|
|
164
|
+
"core-foundation-sys",
|
|
165
|
+
"libc",
|
|
166
|
+
]
|
|
167
|
+
|
|
168
|
+
[[package]]
|
|
169
|
+
name = "core-foundation-sys"
|
|
170
|
+
version = "0.8.7"
|
|
171
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
172
|
+
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
|
|
173
|
+
|
|
158
174
|
[[package]]
|
|
159
175
|
name = "crc32fast"
|
|
160
176
|
version = "1.5.2"
|
|
@@ -551,6 +567,12 @@ version = "1.21.4"
|
|
|
551
567
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
552
568
|
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
|
553
569
|
|
|
570
|
+
[[package]]
|
|
571
|
+
name = "openssl-probe"
|
|
572
|
+
version = "0.2.1"
|
|
573
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
574
|
+
checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe"
|
|
575
|
+
|
|
554
576
|
[[package]]
|
|
555
577
|
name = "parking_lot"
|
|
556
578
|
version = "0.12.5"
|
|
@@ -759,6 +781,18 @@ dependencies = [
|
|
|
759
781
|
"zeroize",
|
|
760
782
|
]
|
|
761
783
|
|
|
784
|
+
[[package]]
|
|
785
|
+
name = "rustls-native-certs"
|
|
786
|
+
version = "0.8.4"
|
|
787
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
788
|
+
checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d"
|
|
789
|
+
dependencies = [
|
|
790
|
+
"openssl-probe",
|
|
791
|
+
"rustls-pki-types",
|
|
792
|
+
"schannel",
|
|
793
|
+
"security-framework",
|
|
794
|
+
]
|
|
795
|
+
|
|
762
796
|
[[package]]
|
|
763
797
|
name = "rustls-pki-types"
|
|
764
798
|
version = "1.15.1"
|
|
@@ -779,12 +813,44 @@ dependencies = [
|
|
|
779
813
|
"untrusted",
|
|
780
814
|
]
|
|
781
815
|
|
|
816
|
+
[[package]]
|
|
817
|
+
name = "schannel"
|
|
818
|
+
version = "0.1.29"
|
|
819
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
820
|
+
checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939"
|
|
821
|
+
dependencies = [
|
|
822
|
+
"windows-sys 0.61.2",
|
|
823
|
+
]
|
|
824
|
+
|
|
782
825
|
[[package]]
|
|
783
826
|
name = "scopeguard"
|
|
784
827
|
version = "1.2.0"
|
|
785
828
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
786
829
|
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
|
787
830
|
|
|
831
|
+
[[package]]
|
|
832
|
+
name = "security-framework"
|
|
833
|
+
version = "3.7.0"
|
|
834
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
835
|
+
checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d"
|
|
836
|
+
dependencies = [
|
|
837
|
+
"bitflags",
|
|
838
|
+
"core-foundation",
|
|
839
|
+
"core-foundation-sys",
|
|
840
|
+
"libc",
|
|
841
|
+
"security-framework-sys",
|
|
842
|
+
]
|
|
843
|
+
|
|
844
|
+
[[package]]
|
|
845
|
+
name = "security-framework-sys"
|
|
846
|
+
version = "2.17.0"
|
|
847
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
848
|
+
checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3"
|
|
849
|
+
dependencies = [
|
|
850
|
+
"core-foundation-sys",
|
|
851
|
+
"libc",
|
|
852
|
+
]
|
|
853
|
+
|
|
788
854
|
[[package]]
|
|
789
855
|
name = "seq-macro"
|
|
790
856
|
version = "0.3.6"
|
|
@@ -1041,9 +1107,10 @@ dependencies = [
|
|
|
1041
1107
|
|
|
1042
1108
|
[[package]]
|
|
1043
1109
|
name = "tonic_rails_native"
|
|
1044
|
-
version = "0.
|
|
1110
|
+
version = "0.6.0"
|
|
1045
1111
|
dependencies = [
|
|
1046
1112
|
"axum",
|
|
1113
|
+
"base64",
|
|
1047
1114
|
"bytes",
|
|
1048
1115
|
"crossbeam-channel",
|
|
1049
1116
|
"flate2",
|
|
@@ -1055,6 +1122,7 @@ dependencies = [
|
|
|
1055
1122
|
"prost",
|
|
1056
1123
|
"prost-types",
|
|
1057
1124
|
"rb-sys",
|
|
1125
|
+
"rustls-native-certs",
|
|
1058
1126
|
"socket2",
|
|
1059
1127
|
"tokio",
|
|
1060
1128
|
"tokio-stream",
|
data/README.md
CHANGED
|
@@ -44,6 +44,10 @@ end
|
|
|
44
44
|
INTERNAL error (the details go to your logs and `Rails.error`).
|
|
45
45
|
Reflection is off unless you turn it on. TLS problems stop the server
|
|
46
46
|
instead of silently falling back to plaintext.
|
|
47
|
+
- **A client too.** Make unary and server-streaming calls to other gRPC
|
|
48
|
+
services, with deadlines that follow your handler's, without the
|
|
49
|
+
`grpc` gem and its fork crashes. See
|
|
50
|
+
[Calling other services](#calling-other-services).
|
|
47
51
|
- **No Rust toolchain needed** on the platforms we ship precompiled gems
|
|
48
52
|
for: Linux and macOS (x86_64 and arm64) and Windows (x64).
|
|
49
53
|
|
|
@@ -401,6 +405,197 @@ const item = await client.getItem({ id: 1n });
|
|
|
401
405
|
warning). `grpc_web_allowed_headers` adds request headers beyond the
|
|
402
406
|
standard ones (`authorization` is already allowed).
|
|
403
407
|
|
|
408
|
+
## Calling other services
|
|
409
|
+
|
|
410
|
+
tonic-rails includes a client for unary and server-streaming calls to
|
|
411
|
+
other gRPC services, so
|
|
412
|
+
an app that serves gRPC (or just calls it) doesn't need the `grpc` gem.
|
|
413
|
+
It runs on the same Rust stack as the server, and it keeps working after
|
|
414
|
+
`fork`. That matters because the `grpc` gem crashes or hangs in forked
|
|
415
|
+
processes (Puma with `preload_app!`, Sidekiq swarm, Resque) unless you
|
|
416
|
+
opt in to its Linux-only fork support and call `GRPC.prefork` and
|
|
417
|
+
friends around every fork yourself.
|
|
418
|
+
|
|
419
|
+
Create a client once and share it; it holds one HTTP/2 connection and is
|
|
420
|
+
safe to use from any thread:
|
|
421
|
+
|
|
422
|
+
```ruby
|
|
423
|
+
# config/initializers/grpc_clients.rb
|
|
424
|
+
ITEMS = Tonic::Rails::Client.new("items.internal:50051", deadline: 2)
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
Then get a stub for a service and call its methods, named in snake_case:
|
|
428
|
+
|
|
429
|
+
```ruby
|
|
430
|
+
items = ITEMS.stub("demo.v1.ItemService")
|
|
431
|
+
|
|
432
|
+
item = items.get_item(Demo::V1::GetItemRequest.new(id: 1)) # a message
|
|
433
|
+
item = items.get_item(id: 1) # or its fields
|
|
434
|
+
item = items.get_item({ id: 1 }, deadline: 0.5, metadata: { "x-request-id" => request.request_id })
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
Stubs are built from the protobuf descriptors in the `_pb.rb` files that
|
|
438
|
+
`tonic_rails:compile_protos` generates, the same ones your handlers use.
|
|
439
|
+
There's no `grpc_tools_ruby_protoc` step and no `_services_pb.rb`.
|
|
440
|
+
Responses are protobuf messages.
|
|
441
|
+
|
|
442
|
+
### Server streaming
|
|
443
|
+
|
|
444
|
+
A server-streaming method returns a stream you read with `each`, or any
|
|
445
|
+
Enumerable method:
|
|
446
|
+
|
|
447
|
+
```ruby
|
|
448
|
+
items.list_items(category: "books").each { |item| index(item) }
|
|
449
|
+
items.list_items(category: "books").first(10) # stops after ten
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
- Nothing is sent until you start reading. Then each message is yielded
|
|
453
|
+
as it arrives, not when the stream ends.
|
|
454
|
+
- Stopping early, with `break`, `first(n)` or an exception, cancels the
|
|
455
|
+
call, and the server sees it cancelled.
|
|
456
|
+
- A slow reader holds the server back. A few messages are read ahead,
|
|
457
|
+
and HTTP/2 flow control stops the server after a couple of megabytes,
|
|
458
|
+
so a fast server can't fill your memory.
|
|
459
|
+
- If the server fails partway through, you get every message it sent
|
|
460
|
+
first, then `Tonic::Rails::RpcError`.
|
|
461
|
+
- Once the stream has ended, `headers` and `trailers` return its
|
|
462
|
+
metadata.
|
|
463
|
+
- A stream can be read once. Call the method again for a new one.
|
|
464
|
+
|
|
465
|
+
Client and bidirectional streaming aren't supported yet; their stub
|
|
466
|
+
methods raise `NotImplementedError`.
|
|
467
|
+
|
|
468
|
+
### Deadlines
|
|
469
|
+
|
|
470
|
+
- `deadline:` on a call is seconds from now (`0.5`) or a `Time`. Without
|
|
471
|
+
one, the client's `deadline:` applies.
|
|
472
|
+
- **Inside a handler, outgoing calls default to the time left on the
|
|
473
|
+
handler's own deadline**, so a service that's out of time doesn't keep
|
|
474
|
+
its dependencies working on an answer nobody will read. If the client
|
|
475
|
+
also has a default deadline, the earlier of the two wins. A deadline on
|
|
476
|
+
the call itself always wins.
|
|
477
|
+
- When a deadline passes, the call raises `DEADLINE_EXCEEDED`, and the
|
|
478
|
+
server sees the call cancelled.
|
|
479
|
+
- A stream's deadline covers the whole stream, including the time your
|
|
480
|
+
block spends on each message. It's set when you call the method, so a
|
|
481
|
+
stream read later still keeps the handler's deadline.
|
|
482
|
+
|
|
483
|
+
Set a deadline. Without one, a call to a server that never answers waits
|
|
484
|
+
forever (or until `Timeout` interrupts it, which also works).
|
|
485
|
+
|
|
486
|
+
### Errors
|
|
487
|
+
|
|
488
|
+
A call that fails raises `Tonic::Rails::RpcError`, the same class your
|
|
489
|
+
handlers raise:
|
|
490
|
+
|
|
491
|
+
```ruby
|
|
492
|
+
begin
|
|
493
|
+
items.get_item(id: 1)
|
|
494
|
+
rescue Tonic::Rails::RpcError => e
|
|
495
|
+
e.code_name # :not_found, :unavailable, :deadline_exceeded, ...
|
|
496
|
+
e.message # the server's status message
|
|
497
|
+
e.details # rich error details (google.rpc.Status), as messages
|
|
498
|
+
e.metadata # trailing metadata
|
|
499
|
+
end
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
Rich error details come back as their message class when its `_pb.rb` is
|
|
503
|
+
loaded, and as `Google::Protobuf::Any` otherwise. A server that can't be
|
|
504
|
+
reached raises `UNAVAILABLE`.
|
|
505
|
+
|
|
506
|
+
If a handler lets the error propagate, its caller receives the same
|
|
507
|
+
status, message, details and trailing metadata. Rescue it and raise your
|
|
508
|
+
own error when those are internal.
|
|
509
|
+
|
|
510
|
+
### Metadata
|
|
511
|
+
|
|
512
|
+
`metadata:` on a call, and on the client for every call, works like
|
|
513
|
+
response metadata in handlers: `-bin` names take binary values. To read
|
|
514
|
+
what the server sent back, pass `return_call: true`:
|
|
515
|
+
|
|
516
|
+
```ruby
|
|
517
|
+
call = items.get_item({ id: 1 }, return_call: true)
|
|
518
|
+
call.response # the message
|
|
519
|
+
call.headers # response headers, -bin values decoded
|
|
520
|
+
call.trailers # trailing metadata
|
|
521
|
+
```
|
|
522
|
+
|
|
523
|
+
### TLS
|
|
524
|
+
|
|
525
|
+
TLS is on unless you turn it off, and plaintext needs an explicit
|
|
526
|
+
`tls: false`:
|
|
527
|
+
|
|
528
|
+
```ruby
|
|
529
|
+
Tonic::Rails::Client.new("items.internal:50051") # system root certificates
|
|
530
|
+
Tonic::Rails::Client.new("items.internal:50051", tls_ca_path: "ca.pem") # your own CA instead
|
|
531
|
+
Tonic::Rails::Client.new("items.internal:50051", tls_ca_path: "ca.pem",
|
|
532
|
+
tls_cert_path: "client.pem", tls_key_path: "client.key") # mutual TLS
|
|
533
|
+
Tonic::Rails::Client.new("localhost:50051", tls: false) # plaintext
|
|
534
|
+
```
|
|
535
|
+
|
|
536
|
+
`tls_server_name:` checks the server's certificate against another name
|
|
537
|
+
than the host you connect to. Unreadable certificates or keys raise when
|
|
538
|
+
the client is created, not on the first call.
|
|
539
|
+
|
|
540
|
+
### Options
|
|
541
|
+
|
|
542
|
+
| Option | Default | |
|
|
543
|
+
|---|---|---|
|
|
544
|
+
| `tls` | `true` | `false` for plaintext |
|
|
545
|
+
| `tls_ca_path` | system roots | CA certificates (PEM) to trust instead |
|
|
546
|
+
| `tls_cert_path`, `tls_key_path` | none | client certificate and key (PEM), for mutual TLS |
|
|
547
|
+
| `tls_server_name` | the host | name to verify the server's certificate against |
|
|
548
|
+
| `deadline` | none | seconds each call may take, unless the call sets its own |
|
|
549
|
+
| `metadata` | `{}` | sent with every call |
|
|
550
|
+
| `max_message_size` | 4 MiB | largest message sent or received |
|
|
551
|
+
| `connect_timeout` | 5 | seconds to wait for a connection |
|
|
552
|
+
|
|
553
|
+
### Fork safety
|
|
554
|
+
|
|
555
|
+
A client created before `fork` works in both processes, on Linux and
|
|
556
|
+
macOS, with nothing to call. Each process opens its own connection on
|
|
557
|
+
its first call, and a child never touches the threads or sockets it
|
|
558
|
+
inherited from its parent. Create clients in initializers, before Puma or
|
|
559
|
+
Sidekiq forks, and use them anywhere.
|
|
560
|
+
|
|
561
|
+
On macOS, also create the first client that uses the system's root
|
|
562
|
+
certificates before forking. Loading them goes through macOS's security
|
|
563
|
+
APIs, which abort in a process forked from a multi-threaded one. Loaded
|
|
564
|
+
once, they're kept and inherited, so clients created later in children
|
|
565
|
+
are fine. Linux has no such limit.
|
|
566
|
+
|
|
567
|
+
### Migrating from the grpc gem
|
|
568
|
+
|
|
569
|
+
Call sites barely change; how you build the stub does:
|
|
570
|
+
|
|
571
|
+
```ruby
|
|
572
|
+
# Before: grpc gem
|
|
573
|
+
stub = Demo::V1::ItemService::Stub.new("items.internal:50051", GRPC::Core::ChannelCredentials.new)
|
|
574
|
+
item = stub.get_item(Demo::V1::GetItemRequest.new(id: 1), deadline: Time.now + 2, metadata: { "x-id" => "1" })
|
|
575
|
+
|
|
576
|
+
# After: tonic-rails
|
|
577
|
+
stub = Tonic::Rails::Client.new("items.internal:50051").stub("demo.v1.ItemService")
|
|
578
|
+
item = stub.get_item(Demo::V1::GetItemRequest.new(id: 1), deadline: Time.now + 2, metadata: { "x-id" => "1" })
|
|
579
|
+
```
|
|
580
|
+
|
|
581
|
+
- `stub` also accepts the grpc gem's service class
|
|
582
|
+
(`Demo::V1::ItemService::Service`), which helps while both gems are
|
|
583
|
+
installed.
|
|
584
|
+
- `deadline:` takes a `Time`, as before, or seconds.
|
|
585
|
+
- `return_op: true` becomes `return_call: true`, which returns the
|
|
586
|
+
response with its headers and trailers instead of an operation.
|
|
587
|
+
- `GRPC::BadStatus` subclasses become one `Tonic::Rails::RpcError`:
|
|
588
|
+
`rescue GRPC::NotFound` becomes `rescue Tonic::Rails::RpcError => e`
|
|
589
|
+
and a check of `e.code_name == :not_found`. The `raise_grpc_error`
|
|
590
|
+
matcher and `assert_grpc_error` accept both.
|
|
591
|
+
- `:this_channel_is_insecure` becomes `tls: false`.
|
|
592
|
+
- Server-streaming methods return a stream instead of an Enumerator.
|
|
593
|
+
Code that calls `each` or other Enumerable methods on the result works
|
|
594
|
+
unchanged; for `next`, call `each` first to get an Enumerator.
|
|
595
|
+
- Client and bidirectional streaming aren't supported yet. Stubs refuse
|
|
596
|
+
those methods with a `NotImplementedError`; keep the grpc gem for them
|
|
597
|
+
for now.
|
|
598
|
+
|
|
404
599
|
## Testing handlers
|
|
405
600
|
|
|
406
601
|
`Tonic::Rails::Testing` calls a handler in-process, with real protobuf
|
|
@@ -470,6 +665,8 @@ end
|
|
|
470
665
|
|--------|---------|-------------|
|
|
471
666
|
| `bind_address` | `"0.0.0.0:50051"` | Address to listen on |
|
|
472
667
|
| `dispatch_threads` | `4` | Ruby threads running handlers; match your database pool |
|
|
668
|
+
| `processes` | `1` | Server processes sharing the port (see [Threads or processes](#threads-or-processes)); Unix only |
|
|
669
|
+
| `on_worker_boot` | `nil` | Callable run in each worker process after fork, given its index |
|
|
473
670
|
| `dispatch_queue_size` | `128` | Calls that may wait for a thread; beyond this, `RESOURCE_EXHAUSTED` |
|
|
474
671
|
| `worker_threads` | CPU count | Tokio threads for the transport |
|
|
475
672
|
| `max_message_size` | 4 MiB | Request and response limit (also caps decompressed size) |
|
|
@@ -490,14 +687,60 @@ end
|
|
|
490
687
|
| `log_requests` | `false` | Log one line per call through `logger` (unexpected exceptions are always logged) |
|
|
491
688
|
| `auto_start` | `false` | Start in a thread of `rails server`; for development (see [auto_start](#auto_start)) |
|
|
492
689
|
|
|
493
|
-
###
|
|
690
|
+
### Threads or processes
|
|
494
691
|
|
|
495
692
|
Handlers run on `dispatch_threads` Ruby threads. Blocking I/O (database,
|
|
496
|
-
HTTP) releases
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
693
|
+
HTTP) releases Ruby's global VM lock, so more threads means more
|
|
694
|
+
concurrent I/O. Ruby code itself runs one thread at a time per process,
|
|
695
|
+
though: a service that spends its time in Ruby (building large
|
|
696
|
+
responses, serializing, computing) tops out at one core however many
|
|
697
|
+
threads it has.
|
|
698
|
+
|
|
699
|
+
So:
|
|
700
|
+
|
|
701
|
+
- **Mostly waiting on I/O?** Add threads. They're cheap, and one process
|
|
702
|
+
goes a long way. Size the database pool to match.
|
|
703
|
+
- **Mostly running Ruby?** Add processes. `processes = 4` runs four
|
|
704
|
+
copies of the server on the same port and uses four cores.
|
|
705
|
+
|
|
706
|
+
```ruby
|
|
707
|
+
config.tonic_rails.processes = ENV.fetch("GRPC_PROCESSES", 4).to_i
|
|
708
|
+
config.tonic_rails.dispatch_threads = 5 # per process
|
|
709
|
+
```
|
|
710
|
+
|
|
711
|
+
Most services mix both. A reasonable start is one process per core and
|
|
712
|
+
a handful of threads in each. Every process has its own database pool,
|
|
713
|
+
so the database sees up to `processes × dispatch_threads` connections.
|
|
714
|
+
|
|
715
|
+
With `processes` above 1, `bin/rails tonic_rails:serve` loads the app
|
|
716
|
+
once and forks the workers, which share memory until they write to it.
|
|
717
|
+
The process you started supervises them:
|
|
718
|
+
|
|
719
|
+
- Workers share one listening socket, and a worker with fewer open
|
|
720
|
+
connections picks up new ones first. gRPC clients keep connections
|
|
721
|
+
open and send every call over them, so a single client connection
|
|
722
|
+
is served by a single worker: spread load with several connections
|
|
723
|
+
or several clients.
|
|
724
|
+
- If a worker dies, it's logged and replaced.
|
|
725
|
+
- SIGTERM or SIGINT to the supervisor drains every worker (health
|
|
726
|
+
reports `NOT_SERVING`, in-flight calls finish) and it exits once all
|
|
727
|
+
have stopped. Workers stop on their own if the supervisor disappears.
|
|
728
|
+
- If a worker fails while starting (a bad configuration, say), the
|
|
729
|
+
server doesn't start.
|
|
730
|
+
|
|
731
|
+
Active Record reconnects in each worker by itself. Reopen anything else
|
|
732
|
+
that can't be shared across `fork` in `on_worker_boot`:
|
|
733
|
+
|
|
734
|
+
```ruby
|
|
735
|
+
config.tonic_rails.on_worker_boot = ->(index) { MyQueueClient.reconnect! }
|
|
736
|
+
```
|
|
737
|
+
|
|
738
|
+
Health checks, `health_check`, `serving!`, `not_serving!` and `stats`
|
|
739
|
+
work per worker: calling `serving!` or `not_serving!` in the supervisor
|
|
740
|
+
after the server has started doesn't reach the workers.
|
|
741
|
+
|
|
742
|
+
Multi-process mode needs `fork()`, so it isn't available on Windows. Run
|
|
743
|
+
one server process per core there, each on its own port.
|
|
501
744
|
|
|
502
745
|
### Running with Puma
|
|
503
746
|
|
|
@@ -513,7 +756,8 @@ web workers. It starts after Puma boots and drains gracefully when Puma
|
|
|
513
756
|
stops or restarts. If the gRPC process dies, Puma shuts down too, so your
|
|
514
757
|
supervisor restarts both. It works with `rails server`, plain `puma`, and
|
|
515
758
|
`preload_app!`. (Not on Windows, which has no `fork()`: run
|
|
516
|
-
`tonic_rails:serve` as its own process there.)
|
|
759
|
+
`tonic_rails:serve` as its own process there.) With `processes` above 1,
|
|
760
|
+
that child process supervises the gRPC workers.
|
|
517
761
|
|
|
518
762
|
For larger deployments, a dedicated `bin/rails tonic_rails:serve` process
|
|
519
763
|
(scaled separately) is still the most flexible option.
|
|
@@ -523,7 +767,7 @@ For larger deployments, a dedicated `bin/rails tonic_rails:serve` process
|
|
|
523
767
|
`auto_start = true` starts the gRPC server in a background thread of
|
|
524
768
|
`rails server` (never in consoles, rake tasks or tests). It's handy in
|
|
525
769
|
development; in production use the Puma plugin or a dedicated process.
|
|
526
|
-
Don't combine it with the plugin
|
|
770
|
+
Don't combine it with the plugin, and it can't run multiple `processes`.
|
|
527
771
|
|
|
528
772
|
## Operations
|
|
529
773
|
|
|
@@ -585,6 +829,11 @@ When a call fails, `event.payload[:exception_object]` is the
|
|
|
585
829
|
`Tonic::Rails::RpcError` sent to the client. If a handler raised something
|
|
586
830
|
unexpected, that error's `cause` is the original exception.
|
|
587
831
|
|
|
832
|
+
Calls made with `Tonic::Rails::Client` publish `request_rpc.tonic_rails`,
|
|
833
|
+
with the same payload plus `:address`, the server called. For a
|
|
834
|
+
server-streaming call, the event covers the whole stream, including the
|
|
835
|
+
time your block spends on each message.
|
|
836
|
+
|
|
588
837
|
## How it works
|
|
589
838
|
|
|
590
839
|
```
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[package]
|
|
2
2
|
name = "tonic_rails_native"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.6.0"
|
|
4
4
|
edition = "2024"
|
|
5
5
|
rust-version = "1.88"
|
|
6
6
|
publish = false
|
|
@@ -21,7 +21,12 @@ crate-type = ["cdylib"]
|
|
|
21
21
|
# and no service definitions are compiled into the extension — routes
|
|
22
22
|
# are registered from Ruby at runtime.
|
|
23
23
|
#
|
|
24
|
-
# socket2
|
|
24
|
+
# socket2 configures the listener (SO_REUSEADDR/SO_REUSEPORT, backlog) and
|
|
25
|
+
# adopts the listening socket worker processes inherit in multi-process mode.
|
|
26
|
+
#
|
|
27
|
+
# The client (`TonicRails::Native::Channel`) uses tonic's Channel with the
|
|
28
|
+
# same raw-byte passthrough. rustls-native-certs loads the system's root
|
|
29
|
+
# certificates for TLS, and base64 turns them into PEM for tonic.
|
|
25
30
|
[dependencies]
|
|
26
31
|
magnus = "0.9"
|
|
27
32
|
rb-sys = "0.9"
|
|
@@ -44,3 +49,5 @@ http-body-util = "0.1"
|
|
|
44
49
|
bytes = "1"
|
|
45
50
|
tower = "0.5"
|
|
46
51
|
flate2 = { version = "1", features = ["rust_backend"] }
|
|
52
|
+
rustls-native-certs = "0.8"
|
|
53
|
+
base64 = "0.22"
|