tonic-rails 0.5.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 234f197083161b48c0fa820f8e9e8bb20138208c131e8467279cd40d9b73d872
4
- data.tar.gz: a5ec7ea58b4325c881045c7550468a19de879b7119212e212500c4632e581f56
3
+ metadata.gz: 8b39b4d814c854c16c5a1f9c7ec05415402fdbb49716846bc3883442931f5165
4
+ data.tar.gz: f4dcedf8d10ec7cd0786232a7637e192ec5dd7746ee15542322465d56bc11436
5
5
  SHA512:
6
- metadata.gz: 59c579e49bfc33f7f551ba522823fac67e253d939723c0fcdf4368560f558094b519a1339ca325d81cd2e06fa3dd51e1b7486b790b11da711ab04a24bfe2d375
7
- data.tar.gz: 229844b0f0d86b85f0545c715bb04c70148ecff829ea699283d5bf0a624aab91e788224af557ad513db559b3f42ed9c381350f0dd00238e0c78f0be67731437f
6
+ metadata.gz: b1b5406e16afb900514ae85ae11350d7a7546c0a08ed27c3c1042622de85b4ea9ac692185e3a0d27792f95ca36e61a1f2ff0b5729bf8e38eb645ed9e45c011ee
7
+ data.tar.gz: 7b3b7386f1c8342cf0c25fce33ea1564c38e1be027788b7b919377354b9a9a53f3c58338e334185d8dd0377f261b9775fadeee1cc4a2b0ac627764737329fd2b
data/CHANGELOG.md CHANGED
@@ -7,6 +7,27 @@ 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
+
10
31
  ## [0.5.0] - 2026-10-11
11
32
 
12
33
  ### Added
@@ -145,7 +166,8 @@ First public release.
145
166
  - Precompiled native gems for Linux (x86_64, aarch64; glibc and musl) and
146
167
  macOS (x86_64, arm64), Ruby 3.3 to 4.0.
147
168
 
148
- [Unreleased]: https://github.com/CodingAnarchy/tonic-rails/compare/v0.5.0...HEAD
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
149
171
  [0.5.0]: https://github.com/CodingAnarchy/tonic-rails/compare/v0.4.0...v0.5.0
150
172
  [0.4.0]: https://github.com/CodingAnarchy/tonic-rails/compare/v0.3.0...v0.4.0
151
173
  [0.3.0]: https://github.com/CodingAnarchy/tonic-rails/compare/v0.2.0...v0.3.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.5.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
@@ -634,6 +829,11 @@ When a call fails, `event.payload[:exception_object]` is the
634
829
  `Tonic::Rails::RpcError` sent to the client. If a handler raised something
635
830
  unexpected, that error's `cause` is the original exception.
636
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
+
637
837
  ## How it works
638
838
 
639
839
  ```
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "tonic_rails_native"
3
- version = "0.5.0"
3
+ version = "0.6.0"
4
4
  edition = "2024"
5
5
  rust-version = "1.88"
6
6
  publish = false
@@ -23,6 +23,10 @@ crate-type = ["cdylib"]
23
23
  #
24
24
  # socket2 configures the listener (SO_REUSEADDR/SO_REUSEPORT, backlog) and
25
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.
26
30
  [dependencies]
27
31
  magnus = "0.9"
28
32
  rb-sys = "0.9"
@@ -45,3 +49,5 @@ http-body-util = "0.1"
45
49
  bytes = "1"
46
50
  tower = "0.5"
47
51
  flate2 = { version = "1", features = ["rust_backend"] }
52
+ rustls-native-certs = "0.8"
53
+ base64 = "0.22"