tonic-rails 0.2.0-x64-mingw-ucrt

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 175412704ded2402eae6f84cf04a27725c546fb51e119bd58ffd3799334a2468
4
+ data.tar.gz: a23d20ff4e88be9ad633d91cd3720922bac33f265f7140fd9c582c7ccd04e158
5
+ SHA512:
6
+ metadata.gz: 8e573f7a4bd52e45f8b4a1e8b7e0690873c2ed08f840df5b9e3d7a81a5016440d7a69c4c202c14069fb9e3e29f4394fd41d938ebdf855ab132e10152f5808627
7
+ data.tar.gz: 1dbe6fa96e87f4801737dea5cf674f6646490731d8ea7c3684eca8b325161edabd0e5287157280095809cc1f5cd595d8a68815501132c9d8d2424e9dfb91d42b
data/CHANGELOG.md ADDED
@@ -0,0 +1,89 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented here. The format is
4
+ based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the
5
+ project follows [Semantic Versioning](https://semver.org/). Until 1.0,
6
+ minor versions may contain breaking changes.
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [0.2.0] - 2026-10-11
11
+
12
+ ### Added
13
+
14
+ - Instrumentation: every RPC publishes a `process_rpc.tonic_rails`
15
+ ActiveSupport::Notifications event (path, service, rpc, metadata, status,
16
+ code) for APM agents, OpenTelemetry and custom subscribers. With no
17
+ subscribers it costs nothing measurable.
18
+ - Response metadata: handlers set `response_metadata` (headers) and
19
+ `trailing_metadata` (trailers, sent on success or failure).
20
+ - Rich errors: `RpcError.new(..., metadata:, details:)` sends trailing
21
+ metadata and a `google.rpc.Status` with packed details in
22
+ `grpc-status-details-bin`, which official gRPC clients decode.
23
+ - Puma plugin: `plugin :tonic_rails` in `config/puma.rb` runs the gRPC
24
+ server as a child process of Puma, in single and cluster mode, with
25
+ graceful stop and restart, and a shutdown if either side dies.
26
+ - Windows: precompiled `x64-mingw-ucrt` gems, tested in CI.
27
+ - `Tonic::Rails::Testing.invoke` returns the response together with its
28
+ headers and trailers.
29
+
30
+ ### Changed
31
+
32
+ - Response headers are sent with the handler's first output instead of
33
+ before the handler runs, so handlers can set them. The client deadline
34
+ applies while waiting.
35
+ - `log_requests` logs one line per RPC through the configured (Rails)
36
+ logger instead of stderr.
37
+ - Transport events from the native extension (rejected calls, expired
38
+ deadlines, refused connections, faults) go through the configured (Rails)
39
+ logger with severities instead of stderr. Handler outcomes, such as a
40
+ `NOT_FOUND` raised by a handler, are no longer logged by the transport;
41
+ use `log_requests` or the instrumentation event for those.
42
+ - `Server#stop` signals shutdown through a queue instead of a pipe, so
43
+ signal handling works the same on every platform.
44
+ - Dependencies: magnus 0.9, google-protobuf 4.36.
45
+
46
+ ## [0.1.0] - 2026-10-10
47
+
48
+ First public release.
49
+
50
+ ### Serving
51
+
52
+ - gRPC server for Rails built on tonic 0.14 (HTTP/2, TLS via rustls,
53
+ keepalive, flow control), with Ruby handlers running on a pool of
54
+ dispatch threads inside the Rails executor.
55
+ - Unary, server-streaming, client-streaming and bidirectional RPCs, with
56
+ backpressure in both directions and incremental streaming.
57
+ - Handlers declared with `rpc_service` (read from the protobuf descriptor
58
+ pool) or individual `rpc` calls; requests as Hashes or, with
59
+ `request_format :message`, protobuf messages; responses as either.
60
+ - Routes registered at runtime: no service definitions are compiled into
61
+ the native extension.
62
+ - Built-in `grpc.health.v1` (per service, flips to NOT_SERVING on
63
+ shutdown) and opt-in `grpc.reflection.v1` generated from loaded
64
+ descriptors.
65
+ - Interceptors, request metadata, client deadlines (`deadline_ms`,
66
+ `deadline_exceeded?`) and shutdown awareness (`cancelled?`).
67
+ - `Tonic::Rails::Testing.call` for in-process handler tests.
68
+ - Railtie: `config.tonic_rails.*` settings, Rails logger and error
69
+ reporter, code reloading support, rake tasks `tonic_rails:serve`,
70
+ `tonic_rails:routes` and `tonic_rails:compile_protos`.
71
+
72
+ ### Production behaviour
73
+
74
+ - Graceful shutdown on SIGTERM/SIGINT: health flips to NOT_SERVING, new
75
+ connections are refused, in-flight calls drain for `drain_timeout`.
76
+ - Limits: `max_message_size` (also caps gzip-decompressed size),
77
+ `max_connections`, `concurrency_limit_per_connection`, request metadata
78
+ size, and `dispatch_queue_size` load shedding (`RESOURCE_EXHAUSTED`).
79
+ - TLS and mutual TLS (`tls_client_ca_path`). Invalid TLS configuration
80
+ stops startup; the server never falls back to plaintext.
81
+ - Unexpected exceptions (in handlers or interceptors) reach clients only
82
+ as `INTERNAL: internal error`; details are logged and reported.
83
+ - `grpc-message` is percent-encoded per the gRPC spec.
84
+ - Precompiled native gems for Linux (x86_64, aarch64; glibc and musl) and
85
+ macOS (x86_64, arm64), Ruby 3.3 to 4.0.
86
+
87
+ [Unreleased]: https://github.com/CodingAnarchy/tonic-rails/compare/v0.2.0...HEAD
88
+ [0.2.0]: https://github.com/CodingAnarchy/tonic-rails/compare/v0.1.0...v0.2.0
89
+ [0.1.0]: https://github.com/CodingAnarchy/tonic-rails/releases/tag/v0.1.0
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Matt Tanous
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,480 @@
1
+ # tonic-rails
2
+
3
+ [![CI](https://github.com/CodingAnarchy/tonic-rails/actions/workflows/ci.yml/badge.svg)](https://github.com/CodingAnarchy/tonic-rails/actions/workflows/ci.yml)
4
+ [![Gem Version](https://badge.fury.io/rb/tonic-rails.svg)](https://rubygems.org/gems/tonic-rails)
5
+
6
+ Serve gRPC from your Rails app, with the transport handled by
7
+ [tonic], Rust's production gRPC stack.
8
+
9
+ Think of it like Puma for gRPC. Puma handles sockets and HTTP so your
10
+ controllers only see requests. tonic-rails does the same for gRPC:
11
+ HTTP/2, TLS, flow control, keepalives, health checks and reflection all
12
+ run in Rust, outside the GVL. Your handlers are plain Ruby classes that
13
+ receive protobuf messages and return them, inside the Rails executor,
14
+ with your models, caches and error reporting all available.
15
+
16
+ ```ruby
17
+ class ItemService
18
+ include Tonic::Rails::Base
19
+ rpc_service "demo.v1.ItemService"
20
+ request_format :message
21
+
22
+ def get_item(request)
23
+ item = Item.find_by(id: request.id)
24
+ raise Tonic::Rails::RpcError.new("item not found", code: :not_found) unless item
25
+
26
+ Demo::V1::Item.new(id: item.id, name: item.name)
27
+ end
28
+ end
29
+ ```
30
+
31
+ ## Why tonic-rails
32
+
33
+ - **Fast where it counts.** Inside a Rails app, light RPCs ran at 5–6×
34
+ the throughput of an equivalent Rails JSON endpoint and 1.4–3× the
35
+ official `grpc` gem, and server streaming at 4–7× the `grpc` gem.
36
+ Handlers dominated by slow queries or heavy object building see smaller
37
+ gains. See [PERF.md](PERF.md).
38
+ - **All four RPC kinds.** Unary, server streaming, client streaming and
39
+ bidirectional streaming, with backpressure in both directions.
40
+ - **Production defaults.** Graceful draining on SIGTERM, health checks
41
+ that flip to NOT_SERVING during shutdown, deadlines, message size and
42
+ connection limits, load shedding, gzip, TLS and mutual TLS.
43
+ - **Safe by default.** Unexpected exceptions reach clients as a generic
44
+ INTERNAL error (the details go to your logs and `Rails.error`).
45
+ Reflection is off unless you turn it on. TLS problems stop the server
46
+ instead of silently falling back to plaintext.
47
+ - **No Rust toolchain needed** on the platforms we ship precompiled gems
48
+ for: Linux and macOS (x86_64 and arm64) and Windows (x64).
49
+
50
+ ## Installation
51
+
52
+ ```ruby
53
+ # Gemfile
54
+ gem "tonic-rails"
55
+ gem "google-protobuf"
56
+ ```
57
+
58
+ Requires Ruby 3.3+ and Rails 7.1+ (or just ActiveSupport, outside Rails).
59
+ On platforms without a precompiled gem, installation builds the
60
+ extension from source, which needs a [Rust toolchain](https://rustup.rs).
61
+
62
+ To generate Ruby classes from your `.proto` files you also need `protoc`
63
+ (the Protocol Buffers compiler) during development.
64
+
65
+ ## Quick start
66
+
67
+ ### 1. Define the service
68
+
69
+ ```protobuf
70
+ // app/protos/items.proto
71
+ syntax = "proto3";
72
+ package demo.v1;
73
+
74
+ message Item { int64 id = 1; string name = 2; }
75
+ message GetItemRequest { int64 id = 1; }
76
+
77
+ service ItemService {
78
+ rpc GetItem(GetItemRequest) returns (Item);
79
+ }
80
+ ```
81
+
82
+ Generate the Ruby classes:
83
+
84
+ ```sh
85
+ bin/rails tonic_rails:compile_protos # app/protos → lib/grpc
86
+ ```
87
+
88
+ Generated files `require` each other by name, so keep them out of the
89
+ autoload paths and put their directory on the load path:
90
+
91
+ ```ruby
92
+ # config/application.rb
93
+ $LOAD_PATH.unshift(File.expand_path("../lib/grpc", __dir__))
94
+
95
+ module MyApp
96
+ class Application < Rails::Application
97
+ config.autoload_lib(ignore: %w[grpc])
98
+ end
99
+ end
100
+ ```
101
+
102
+ ### 2. Write a handler
103
+
104
+ Handlers live anywhere Rails autoloads from; `app/grpc` is a good home.
105
+ Each RPC maps to a snake_case method.
106
+
107
+ ```ruby
108
+ # app/grpc/item_service.rb
109
+ require "items_pb"
110
+
111
+ class ItemService
112
+ include Tonic::Rails::Base
113
+
114
+ rpc_service "demo.v1.ItemService" # every RPC, read from the descriptor
115
+ request_format :message # receive protobuf messages
116
+
117
+ def get_item(request)
118
+ item = Item.find(request.id)
119
+ Demo::V1::Item.new(id: item.id, name: item.name)
120
+ rescue ActiveRecord::RecordNotFound
121
+ raise Tonic::Rails::RpcError.new("item #{request.id} not found", code: :not_found)
122
+ end
123
+ end
124
+ ```
125
+
126
+ ### 3. Register and run
127
+
128
+ Register handlers in `to_prepare` so they work with code reloading:
129
+
130
+ ```ruby
131
+ # config/initializers/tonic_rails.rb
132
+ Rails.application.config.to_prepare do
133
+ Tonic::Rails.server.register(ItemService)
134
+ end
135
+ ```
136
+
137
+ ```sh
138
+ bin/rails tonic_rails:serve # serves on 0.0.0.0:50051
139
+ bin/rails tonic_rails:routes # lists registered methods
140
+ ```
141
+
142
+ Run it as its own process (a separate Procfile entry, container or
143
+ Kubernetes deployment) next to your web server, or let Puma run it for
144
+ you (see [Running with Puma](#running-with-puma)).
145
+
146
+ The [example app](example/) is a complete, runnable version of all of the
147
+ above.
148
+
149
+ ## Handlers
150
+
151
+ ### Requests and responses
152
+
153
+ By default handlers receive a string-keyed Hash. With
154
+ `request_format :message` they receive the decoded protobuf message
155
+ instead, which is faster and type-safe. Either way, return a protobuf
156
+ message or a Hash:
157
+
158
+ ```ruby
159
+ def get_item(request)
160
+ { "id" => request.id, "name" => "Widget" } # a Hash works too
161
+ end
162
+ ```
163
+
164
+ A fresh handler instance serves each call, so instance variables never
165
+ leak between requests.
166
+
167
+ ### Declaring RPCs
168
+
169
+ `rpc_service "package.Service"` reads every RPC (and its streaming kind)
170
+ from the generated descriptor. To declare RPCs one at a time instead:
171
+
172
+ ```ruby
173
+ class ItemService
174
+ include Tonic::Rails::Base
175
+ service "demo.v1.ItemService"
176
+
177
+ rpc :GetItem, Demo::V1::GetItemRequest, Demo::V1::Item
178
+ rpc :ListItems, Demo::V1::ListItemsRequest, Demo::V1::Item, stream: :server
179
+ end
180
+ ```
181
+
182
+ RPCs declared without a handler method answer `UNIMPLEMENTED`.
183
+
184
+ ### Streaming
185
+
186
+ Server streaming: return any Enumerable. Messages go out as you produce
187
+ them, and if the client reads slowly your handler waits (without holding
188
+ the GVL) instead of buffering everything in memory.
189
+
190
+ ```ruby
191
+ def list_items(request)
192
+ Enumerator.new do |stream|
193
+ Item.where(tag: request.tag).find_each do |item|
194
+ break if cancelled? # the server is shutting down
195
+ stream << Demo::V1::Item.new(id: item.id, name: item.name)
196
+ end
197
+ end
198
+ end
199
+ ```
200
+
201
+ Client streaming: the argument is an Enumerator that yields each request
202
+ as it arrives.
203
+
204
+ ```ruby
205
+ def import_items(requests)
206
+ created = requests.count { |r| Item.create!(name: r.name) }
207
+ Demo::V1::ImportItemsResponse.new(created: created)
208
+ end
209
+ ```
210
+
211
+ Bidirectional streaming combines both: take the request Enumerator and
212
+ return an Enumerable. Responses can be sent before the client finishes
213
+ sending.
214
+
215
+ ```ruby
216
+ def chat(messages)
217
+ Enumerator.new do |out|
218
+ messages.each { |m| out << Demo::V1::Reply.new(text: m.text.upcase) }
219
+ end
220
+ end
221
+ ```
222
+
223
+ ### Errors
224
+
225
+ Raise `Tonic::Rails::RpcError` to end a call with a status:
226
+
227
+ ```ruby
228
+ raise Tonic::Rails::RpcError.new("name is required", code: :invalid_argument)
229
+ ```
230
+
231
+ Codes are the standard gRPC names (`:not_found`, `:permission_denied`,
232
+ `:unauthenticated`, `:unavailable`, and so on) or their numbers. The
233
+ message goes to the client, so keep internal details out of it.
234
+
235
+ Any other exception becomes `INTERNAL` with the message `internal error`.
236
+ The real exception is logged and passed to `Rails.error.report`, so your
237
+ error tracker sees it.
238
+
239
+ ### Metadata, deadlines and shutdown
240
+
241
+ ```ruby
242
+ def get_item(request)
243
+ token = grpc_metadata["authorization"] # request headers, lower-case
244
+ deadline_ms # ms left before the client's deadline, or nil
245
+ deadline_exceeded? # true once it has passed
246
+ cancelled? # true while the server shuts down
247
+ end
248
+ ```
249
+
250
+ When the deadline passes, the client gets `DEADLINE_EXCEEDED` right away;
251
+ check `deadline_exceeded?` in long loops to stop wasted work.
252
+
253
+ ### Response metadata
254
+
255
+ Set response headers and trailers from a handler:
256
+
257
+ ```ruby
258
+ def get_item(request)
259
+ response_metadata["x-cache"] = "hit" # headers
260
+ trailing_metadata["x-cost-ms"] = "3" # trailers, sent with the status
261
+ response_metadata["x-trace-bin"] = trace_bytes # -bin names take binary values
262
+ Demo::V1::Item.new(id: request.id)
263
+ end
264
+ ```
265
+
266
+ For streaming responses, set headers before the first message is sent.
267
+ Trailers are sent whether the call succeeds or fails.
268
+
269
+ ### Rich errors
270
+
271
+ `RpcError` can carry trailing metadata and
272
+ [rich error details](https://grpc.io/docs/guides/error/#richer-error-model):
273
+ protobuf messages packed into a `google.rpc.Status` in the standard
274
+ `grpc-status-details-bin` trailer, which official gRPC clients decode for
275
+ you.
276
+
277
+ ```ruby
278
+ raise Tonic::Rails::RpcError.new(
279
+ "invalid item",
280
+ code: :invalid_argument,
281
+ metadata: { "x-request-id" => request_id },
282
+ details: [Google::Rpc::BadRequest.new(field_violations: violations)],
283
+ )
284
+ ```
285
+
286
+ ### Interceptors
287
+
288
+ Interceptors wrap every call, outermost first. Call `next_call` to
289
+ continue, or raise to reject:
290
+
291
+ ```ruby
292
+ Tonic::Rails.configure do |config|
293
+ config.interceptors << lambda do |path, metadata, next_call|
294
+ unless valid_token?(metadata["authorization"])
295
+ raise Tonic::Rails::RpcError.new("invalid token", code: :unauthenticated)
296
+ end
297
+ next_call.call
298
+ end
299
+ end
300
+ ```
301
+
302
+ ## Testing handlers
303
+
304
+ `Tonic::Rails::Testing` calls a handler in-process, with real protobuf
305
+ encoding, no network and no native extension:
306
+
307
+ ```ruby
308
+ require "tonic/rails/testing"
309
+
310
+ response = Tonic::Rails::Testing.call(ItemService, :GetItem, { id: 1 })
311
+ expect(response.name).to eq("Widget")
312
+
313
+ expect { Tonic::Rails::Testing.call(ItemService, :GetItem, { id: 0 }) }
314
+ .to raise_error(Tonic::Rails::RpcError) { |e| expect(e.code_name).to eq(:not_found) }
315
+
316
+ # Streaming: pass an Array of requests, get an Array of responses
317
+ Tonic::Rails::Testing.call(ItemService, :ImportItems, [{ name: "a" }, { name: "b" }])
318
+
319
+ # Response metadata
320
+ result = Tonic::Rails::Testing.invoke(ItemService, :GetItem, { id: 1 })
321
+ result.headers["x-cache"] # => "hit"
322
+ result.trailers["x-cost-ms"] # => "3"
323
+ ```
324
+
325
+ ## Configuration
326
+
327
+ Set options in `config/application.rb` or an environment file:
328
+
329
+ ```ruby
330
+ config.tonic_rails.bind_address = "0.0.0.0:50051"
331
+ config.tonic_rails.dispatch_threads = ENV.fetch("RAILS_MAX_THREADS", 5).to_i
332
+ ```
333
+
334
+ or with a block (which runs after, and wins over, `config.tonic_rails`):
335
+
336
+ ```ruby
337
+ Tonic::Rails.configure do |config|
338
+ config.tls_cert_path = "/etc/tls/tls.crt"
339
+ config.tls_key_path = "/etc/tls/tls.key"
340
+ end
341
+ ```
342
+
343
+ | Option | Default | Description |
344
+ |--------|---------|-------------|
345
+ | `bind_address` | `"0.0.0.0:50051"` | Address to listen on |
346
+ | `dispatch_threads` | `4` | Ruby threads running handlers; match your database pool |
347
+ | `dispatch_queue_size` | `128` | Calls that may wait for a thread; beyond this, `RESOURCE_EXHAUSTED` |
348
+ | `worker_threads` | CPU count | Tokio threads for the transport |
349
+ | `max_message_size` | 4 MiB | Request and response limit (also caps decompressed size) |
350
+ | `max_connections` | `64` | Concurrent connections; `0` = unlimited |
351
+ | `concurrency_limit_per_connection` | `32` | Concurrent streams per connection; `0` = unlimited |
352
+ | `tls_cert_path`, `tls_key_path` | `nil` | PEM certificate and key; set both to enable TLS |
353
+ | `tls_client_ca_path` | `nil` | PEM CA bundle; requires client certificates (mutual TLS) |
354
+ | `drain_timeout` | `10` | Seconds to let in-flight calls finish on shutdown |
355
+ | `keepalive_interval`, `keepalive_timeout` | `15`, `5` | HTTP/2 PING interval and timeout, seconds |
356
+ | `compression` | `false` | gzip responses over 1 KiB for clients that accept it |
357
+ | `reflection` | `false` | Serve `grpc.reflection.v1`; exposes your schema |
358
+ | `interceptors` | `[]` | See [Interceptors](#interceptors) |
359
+ | `log_requests` | `false` | Log one line per call through `logger` (unexpected exceptions are always logged) |
360
+ | `auto_start` | `false` | Start in a thread of `rails server`; for development (see [auto_start](#auto_start)) |
361
+
362
+ ### Sizing threads
363
+
364
+ Handlers run on `dispatch_threads` Ruby threads. Blocking I/O (database,
365
+ HTTP) releases the GVL, so more threads means more concurrent I/O. CPU
366
+ work does not parallelise within a process. Set threads to the
367
+ concurrency your I/O needs, size the database pool to match, and add
368
+ processes for CPU-heavy services: the listener uses `SO_REUSEPORT`, so
369
+ several processes can bind the same port.
370
+
371
+ ### Running with Puma
372
+
373
+ Add the plugin to `config/puma.rb`:
374
+
375
+ ```ruby
376
+ plugin :tonic_rails
377
+ ```
378
+
379
+ Puma then runs the gRPC server as one child process, in single and cluster
380
+ mode alike, so gRPC gets its own process and GVL instead of competing with
381
+ web workers. It starts after Puma boots and drains gracefully when Puma
382
+ stops or restarts. If the gRPC process dies, Puma shuts down too, so your
383
+ supervisor restarts both. It works with `rails server`, plain `puma`, and
384
+ `preload_app!`. (Not on Windows, which has no `fork()`: run
385
+ `tonic_rails:serve` as its own process there.)
386
+
387
+ For larger deployments, a dedicated `bin/rails tonic_rails:serve` process
388
+ (scaled separately) is still the most flexible option.
389
+
390
+ ### auto_start
391
+
392
+ `auto_start = true` starts the gRPC server in a background thread of
393
+ `rails server` (never in consoles, rake tasks or tests). It's handy in
394
+ development; in production use the Puma plugin or a dedicated process.
395
+ Don't combine it with the plugin.
396
+
397
+ ## Operations
398
+
399
+ - **Health checks.** `grpc.health.v1.Health` reports `SERVING` for the
400
+ server (`""`) and every registered service, and flips to `NOT_SERVING`
401
+ as soon as shutdown begins. Point your load balancer or Kubernetes
402
+ gRPC probes at it.
403
+ - **Graceful shutdown.** On SIGTERM or SIGINT the server stops
404
+ accepting connections, tells clients to go away (HTTP/2 GOAWAY), lets
405
+ in-flight calls finish for up to `drain_timeout` seconds, then exits.
406
+ - **Load shedding.** When every dispatch thread is busy and the queue is
407
+ full, new calls fail fast with `RESOURCE_EXHAUSTED` instead of piling up.
408
+ - **Instrumentation.** Every RPC publishes a `process_rpc.tonic_rails`
409
+ event through `ActiveSupport::Notifications`, so APM agents,
410
+ OpenTelemetry and your own subscribers can trace calls. See
411
+ [Instrumentation](#instrumentation).
412
+ - **Logs.** Everything goes through your (Rails) logger. With
413
+ `log_requests = true`, each RPC logs one line, e.g.
414
+ `gRPC /demo.v1.ItemService/GetItem OK in 1.4ms`. Calls the transport
415
+ rejects before reaching Ruby (unknown methods, a full queue, expired
416
+ deadlines, oversized messages, refused connections) are always logged,
417
+ at info for client-caused problems, warn for overload and error for faults.
418
+ - **Stats.** `Tonic::Rails.server.stats` returns counters for totals,
419
+ successes, errors, cancellations, deadline and resource errors, active
420
+ connections and queued calls.
421
+ - **Reflection.** Turn on `reflection` in development to use
422
+ [grpcurl](https://github.com/fullstorydev/grpcurl) without proto files.
423
+
424
+ ## Instrumentation
425
+
426
+ Every RPC is wrapped in an `ActiveSupport::Notifications` event, inside the
427
+ Rails executor, so subscribers see the same context as your handler:
428
+
429
+ ```ruby
430
+ ActiveSupport::Notifications.subscribe("process_rpc.tonic_rails") do |event|
431
+ event.payload[:path] # "/demo.v1.ItemService/GetItem"
432
+ event.payload[:service] # "demo.v1.ItemService"
433
+ event.payload[:rpc] # "GetItem"
434
+ event.payload[:metadata] # request metadata
435
+ event.payload[:status] # :ok, :not_found, :internal, ...
436
+ event.payload[:code] # numeric gRPC status
437
+ event.duration # milliseconds
438
+ end
439
+ ```
440
+
441
+ When a call fails, `event.payload[:exception_object]` is the
442
+ `Tonic::Rails::RpcError` sent to the client. If a handler raised something
443
+ unexpected, that error's `cause` is the original exception.
444
+
445
+ ## How it works
446
+
447
+ ```
448
+ client ──HTTP/2──▶ tonic (Rust, no GVL)
449
+ ├─ TLS, flow control, keepalive, limits
450
+ ├─ grpc.health.v1, grpc.reflection.v1
451
+ └─ everything else ─▶ queue ─▶ Ruby dispatch thread
452
+ (takes the GVL)
453
+ decode → handler → encode
454
+ ```
455
+
456
+ Rust never decodes your messages; it passes raw protobuf bytes to Ruby,
457
+ where the `google-protobuf` C extension decodes them once. Routes come
458
+ from the handlers you register, so nothing about your services is
459
+ compiled into the extension.
460
+
461
+ ## Development
462
+
463
+ ```sh
464
+ bundle install
465
+ bundle exec rake compile # build the extension (needs Rust)
466
+ bundle exec rake # Ruby specs + Rust unit tests
467
+ bundle exec rubocop
468
+ bundle exec rake cargo:lint
469
+ ```
470
+
471
+ The integration specs start real servers and call them with the official
472
+ `grpc` gem; the reflection specs also need `grpcurl` on your PATH.
473
+ Benchmarks live in [bench/](bench/) (see [PERF.md](PERF.md)). See
474
+ [CONTRIBUTING.md](CONTRIBUTING.md) for more.
475
+
476
+ ## License
477
+
478
+ MIT. See [LICENSE](LICENSE).
479
+
480
+ [tonic]: https://github.com/hyperium/tonic