tonic-rails 0.1.0-aarch64-linux-gnu → 0.2.0-aarch64-linux-gnu
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 +39 -2
- data/README.md +100 -11
- data/lib/puma/plugin/tonic_rails.rb +116 -0
- data/lib/tonic/rails/3.3/tonic_rails_native.so +0 -0
- data/lib/tonic/rails/3.4/tonic_rails_native.so +0 -0
- data/lib/tonic/rails/4.0/tonic_rails_native.so +0 -0
- data/lib/tonic/rails/base.rb +69 -22
- data/lib/tonic/rails/configuration.rb +2 -1
- data/lib/tonic/rails/instrumentation.rb +78 -0
- data/lib/tonic/rails/metadata.rb +50 -0
- data/lib/tonic/rails/railtie.rb +2 -2
- data/lib/tonic/rails/rpc_error.rb +73 -6
- data/lib/tonic/rails/server.rb +59 -12
- data/lib/tonic/rails/testing.rb +32 -3
- 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: 36f3246aaef4e60039d233f9eb7858b1df32b48b83779b57c4586729c540e5f8
|
|
4
|
+
data.tar.gz: a269e61105e74f82fbd9741ce07cbc458f9b0a777ad5cb5c711fe475d701882d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 056a8b506f16b4ccd6cda18d07fe9741be5d5c4532e97a213f9338ec374a6d2c43f9b8aa32de2c4c4b206cb7c63610b57184c67bdd7bcd8c325d8cc08e13eff1
|
|
7
|
+
data.tar.gz: 63983633cb2263aad99258a50a79185315176ba0de3ee48ca33a9f120b16ab5e9692d0de31dc9252a2cc2f64dfb0fe70529744448d3169f2d17c3f09cf2c660e
|
data/CHANGELOG.md
CHANGED
|
@@ -7,7 +7,43 @@ minor versions may contain breaking changes.
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
-
## [0.
|
|
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
|
|
11
47
|
|
|
12
48
|
First public release.
|
|
13
49
|
|
|
@@ -48,5 +84,6 @@ First public release.
|
|
|
48
84
|
- Precompiled native gems for Linux (x86_64, aarch64; glibc and musl) and
|
|
49
85
|
macOS (x86_64, arm64), Ruby 3.3 to 4.0.
|
|
50
86
|
|
|
51
|
-
[Unreleased]: https://github.com/CodingAnarchy/tonic-rails/compare/v0.
|
|
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
|
|
52
89
|
[0.1.0]: https://github.com/CodingAnarchy/tonic-rails/releases/tag/v0.1.0
|
data/README.md
CHANGED
|
@@ -30,10 +30,11 @@ end
|
|
|
30
30
|
|
|
31
31
|
## Why tonic-rails
|
|
32
32
|
|
|
33
|
-
- **Fast where it counts.**
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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).
|
|
37
38
|
- **All four RPC kinds.** Unary, server streaming, client streaming and
|
|
38
39
|
bidirectional streaming, with backpressure in both directions.
|
|
39
40
|
- **Production defaults.** Graceful draining on SIGTERM, health checks
|
|
@@ -44,7 +45,7 @@ end
|
|
|
44
45
|
Reflection is off unless you turn it on. TLS problems stop the server
|
|
45
46
|
instead of silently falling back to plaintext.
|
|
46
47
|
- **No Rust toolchain needed** on the platforms we ship precompiled gems
|
|
47
|
-
for
|
|
48
|
+
for: Linux and macOS (x86_64 and arm64) and Windows (x64).
|
|
48
49
|
|
|
49
50
|
## Installation
|
|
50
51
|
|
|
@@ -139,7 +140,8 @@ bin/rails tonic_rails:routes # lists registered methods
|
|
|
139
140
|
```
|
|
140
141
|
|
|
141
142
|
Run it as its own process (a separate Procfile entry, container or
|
|
142
|
-
Kubernetes deployment) next to your web server
|
|
143
|
+
Kubernetes deployment) next to your web server, or let Puma run it for
|
|
144
|
+
you (see [Running with Puma](#running-with-puma)).
|
|
143
145
|
|
|
144
146
|
The [example app](example/) is a complete, runnable version of all of the
|
|
145
147
|
above.
|
|
@@ -248,6 +250,39 @@ end
|
|
|
248
250
|
When the deadline passes, the client gets `DEADLINE_EXCEEDED` right away;
|
|
249
251
|
check `deadline_exceeded?` in long loops to stop wasted work.
|
|
250
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
|
+
|
|
251
286
|
### Interceptors
|
|
252
287
|
|
|
253
288
|
Interceptors wrap every call, outermost first. Call `next_call` to
|
|
@@ -280,6 +315,11 @@ expect { Tonic::Rails::Testing.call(ItemService, :GetItem, { id: 0 }) }
|
|
|
280
315
|
|
|
281
316
|
# Streaming: pass an Array of requests, get an Array of responses
|
|
282
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"
|
|
283
323
|
```
|
|
284
324
|
|
|
285
325
|
## Configuration
|
|
@@ -316,8 +356,8 @@ end
|
|
|
316
356
|
| `compression` | `false` | gzip responses over 1 KiB for clients that accept it |
|
|
317
357
|
| `reflection` | `false` | Serve `grpc.reflection.v1`; exposes your schema |
|
|
318
358
|
| `interceptors` | `[]` | See [Interceptors](#interceptors) |
|
|
319
|
-
| `log_requests` | `false` | Log
|
|
320
|
-
| `auto_start` | `false` | Start
|
|
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)) |
|
|
321
361
|
|
|
322
362
|
### Sizing threads
|
|
323
363
|
|
|
@@ -328,13 +368,31 @@ concurrency your I/O needs, size the database pool to match, and add
|
|
|
328
368
|
processes for CPU-heavy services: the listener uses `SO_REUSEPORT`, so
|
|
329
369
|
several processes can bind the same port.
|
|
330
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
|
+
|
|
331
390
|
### auto_start
|
|
332
391
|
|
|
333
392
|
`auto_start = true` starts the gRPC server in a background thread of
|
|
334
393
|
`rails server` (never in consoles, rake tasks or tests). It's handy in
|
|
335
|
-
development
|
|
336
|
-
|
|
337
|
-
start one per worker.
|
|
394
|
+
development; in production use the Puma plugin or a dedicated process.
|
|
395
|
+
Don't combine it with the plugin.
|
|
338
396
|
|
|
339
397
|
## Operations
|
|
340
398
|
|
|
@@ -347,12 +405,43 @@ start one per worker.
|
|
|
347
405
|
in-flight calls finish for up to `drain_timeout` seconds, then exits.
|
|
348
406
|
- **Load shedding.** When every dispatch thread is busy and the queue is
|
|
349
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.
|
|
350
418
|
- **Stats.** `Tonic::Rails.server.stats` returns counters for totals,
|
|
351
419
|
successes, errors, cancellations, deadline and resource errors, active
|
|
352
420
|
connections and queued calls.
|
|
353
421
|
- **Reflection.** Turn on `reflection` in development to use
|
|
354
422
|
[grpcurl](https://github.com/fullstorydev/grpcurl) without proto files.
|
|
355
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
|
+
|
|
356
445
|
## How it works
|
|
357
446
|
|
|
358
447
|
```
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "puma/plugin"
|
|
4
|
+
|
|
5
|
+
# Runs the tonic-rails gRPC server alongside Puma:
|
|
6
|
+
#
|
|
7
|
+
# # config/puma.rb
|
|
8
|
+
# plugin :tonic_rails
|
|
9
|
+
#
|
|
10
|
+
# The gRPC server runs as one child process of the Puma master, in single
|
|
11
|
+
# and cluster mode alike, so it has its own GVL and never competes with web
|
|
12
|
+
# workers. It starts once Puma has booted, drains gracefully when Puma
|
|
13
|
+
# stops or restarts, stops itself if Puma goes away, and if it dies
|
|
14
|
+
# unexpectedly Puma shuts down too, so a supervisor can restart both.
|
|
15
|
+
Puma::Plugin.create do
|
|
16
|
+
def start(launcher)
|
|
17
|
+
@launcher = launcher
|
|
18
|
+
unless Process.respond_to?(:fork)
|
|
19
|
+
log "tonic-rails: the Puma plugin needs fork(), which this platform lacks; " \
|
|
20
|
+
"run `bin/rails tonic_rails:serve` as a separate process instead"
|
|
21
|
+
return
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
@puma_pid = Process.pid
|
|
25
|
+
@grpc_pid = nil
|
|
26
|
+
|
|
27
|
+
events = launcher.events
|
|
28
|
+
if events.respond_to?(:after_booted) # Puma 7+
|
|
29
|
+
events.after_booted { start_grpc }
|
|
30
|
+
events.after_stopped { stop_grpc }
|
|
31
|
+
events.before_restart { stop_grpc }
|
|
32
|
+
else
|
|
33
|
+
events.on_booted { start_grpc }
|
|
34
|
+
events.on_stopped { stop_grpc }
|
|
35
|
+
events.on_restart { stop_grpc }
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
in_background { monitor_grpc }
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def start_grpc
|
|
44
|
+
@grpc_pid = fork { run_grpc_server }
|
|
45
|
+
log "tonic-rails: gRPC server started (pid #{@grpc_pid})"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def run_grpc_server
|
|
49
|
+
# The web listeners belong to Puma; don't hold them open in this process.
|
|
50
|
+
@launcher.binder.ios.each { |io| io.close rescue nil } # rubocop:disable Style/RescueModifier
|
|
51
|
+
load_rails_app
|
|
52
|
+
Thread.new { stop_when_puma_exits }
|
|
53
|
+
Tonic::Rails.server.start
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# `rails server` and preload_app! boot the app before forking; plain
|
|
57
|
+
# `puma` in cluster mode does not, so boot it here.
|
|
58
|
+
def load_rails_app
|
|
59
|
+
return if defined?(Rails.application) && Rails.application&.initialized?
|
|
60
|
+
|
|
61
|
+
require File.expand_path("config/environment", Dir.pwd)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Puma fires its stop hooks from its TERM trap handler, so this must
|
|
65
|
+
# not block indefinitely: allow the server's drain timeout plus a margin,
|
|
66
|
+
# then kill it.
|
|
67
|
+
def stop_grpc
|
|
68
|
+
pid = @grpc_pid
|
|
69
|
+
return unless pid
|
|
70
|
+
|
|
71
|
+
@grpc_pid = nil
|
|
72
|
+
log "tonic-rails: stopping gRPC server (pid #{pid})"
|
|
73
|
+
Process.kill(:TERM, pid)
|
|
74
|
+
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + stop_timeout
|
|
75
|
+
until Process.waitpid(pid, Process::WNOHANG)
|
|
76
|
+
if Process.clock_gettime(Process::CLOCK_MONOTONIC) > deadline
|
|
77
|
+
log "tonic-rails: gRPC server (pid #{pid}) did not stop in #{stop_timeout}s; killing it"
|
|
78
|
+
Process.kill(:KILL, pid)
|
|
79
|
+
Process.wait(pid)
|
|
80
|
+
break
|
|
81
|
+
end
|
|
82
|
+
sleep 0.05
|
|
83
|
+
end
|
|
84
|
+
rescue Errno::ESRCH, Errno::ECHILD
|
|
85
|
+
nil
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def stop_timeout
|
|
89
|
+
drain = defined?(Tonic::Rails) ? Tonic::Rails.configuration.drain_timeout.to_f : 10.0
|
|
90
|
+
drain + 5
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def monitor_grpc
|
|
94
|
+
loop do
|
|
95
|
+
sleep 1
|
|
96
|
+
pid = @grpc_pid
|
|
97
|
+
next unless pid && Process.waitpid(pid, Process::WNOHANG)
|
|
98
|
+
|
|
99
|
+
@grpc_pid = nil
|
|
100
|
+
log "tonic-rails: gRPC server (pid #{pid}) exited unexpectedly; stopping Puma"
|
|
101
|
+
Process.kill(:TERM, @puma_pid)
|
|
102
|
+
break
|
|
103
|
+
rescue Errno::ECHILD
|
|
104
|
+
nil
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def stop_when_puma_exits
|
|
109
|
+
sleep 1 while Process.ppid == @puma_pid
|
|
110
|
+
Tonic::Rails.server.stop
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def log(message)
|
|
114
|
+
@launcher.log_writer.log(message)
|
|
115
|
+
end
|
|
116
|
+
end
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
data/lib/tonic/rails/base.rb
CHANGED
|
@@ -111,6 +111,21 @@ module Tonic
|
|
|
111
111
|
# @return [Hash{String=>String}]
|
|
112
112
|
attr_reader :grpc_metadata
|
|
113
113
|
|
|
114
|
+
# Response headers (initial metadata) for this call. For streaming
|
|
115
|
+
# responses, set them before the first message; for unary RPCs,
|
|
116
|
+
# anywhere in the handler. `-bin` names take binary values.
|
|
117
|
+
# @return [Hash]
|
|
118
|
+
def response_metadata
|
|
119
|
+
@response_metadata ||= {}
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Trailing metadata, sent with the final status, whether the call
|
|
123
|
+
# succeeds or fails.
|
|
124
|
+
# @return [Hash]
|
|
125
|
+
def trailing_metadata
|
|
126
|
+
@trailing_metadata ||= {}
|
|
127
|
+
end
|
|
128
|
+
|
|
114
129
|
# Milliseconds left before the client's deadline, or nil if the
|
|
115
130
|
# client did not set one.
|
|
116
131
|
# @return [Integer, nil]
|
|
@@ -143,39 +158,52 @@ module Tonic
|
|
|
143
158
|
raise RpcError.new("method #{rpc_name} not implemented", code: :unimplemented) unless respond_to?(spec[:handler])
|
|
144
159
|
|
|
145
160
|
@grpc_metadata = metadata
|
|
161
|
+
@grpc_sender = sender
|
|
162
|
+
@grpc_headers_sent = false
|
|
146
163
|
@grpc_deadline_at = timeout_ms && (Process.clock_gettime(Process::CLOCK_MONOTONIC) + (timeout_ms / 1000.0))
|
|
147
164
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
while (message = read_request(reader, spec))
|
|
151
|
-
y << message
|
|
152
|
-
end
|
|
153
|
-
end
|
|
154
|
-
else
|
|
155
|
-
read_request(reader, spec) || raise(RpcError.new("missing request message", code: :invalid_argument))
|
|
156
|
-
end
|
|
157
|
-
|
|
158
|
-
result = public_send(spec[:handler], request)
|
|
159
|
-
|
|
160
|
-
if spec[:response_stream]
|
|
161
|
-
unless result.respond_to?(:each)
|
|
162
|
-
raise TypeError, "streaming handler #{self.class}##{spec[:handler]} must return an Enumerable, got #{result.class}"
|
|
163
|
-
end
|
|
164
|
-
|
|
165
|
-
result.each { |item| push_response(sender, encode_response(item, spec)) }
|
|
166
|
-
nil
|
|
167
|
-
else
|
|
168
|
-
encode_response(result, spec)
|
|
169
|
-
end
|
|
165
|
+
result = public_send(spec[:handler], build_request(reader, spec))
|
|
166
|
+
respond(result, spec, sender)
|
|
170
167
|
rescue RpcError
|
|
171
168
|
raise
|
|
172
169
|
rescue StandardError => e
|
|
173
170
|
Tonic::Rails.log_exception(e, "#{self.class}##{rpc_name}")
|
|
174
171
|
raise RpcError.new("internal error", code: :internal)
|
|
172
|
+
ensure
|
|
173
|
+
attach_trailing_metadata
|
|
175
174
|
end
|
|
176
175
|
|
|
177
176
|
private
|
|
178
177
|
|
|
178
|
+
def build_request(reader, spec)
|
|
179
|
+
unless spec[:request_stream]
|
|
180
|
+
return read_request(reader, spec) || raise(RpcError.new("missing request message", code: :invalid_argument))
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
Enumerator.new do |y|
|
|
184
|
+
while (message = read_request(reader, spec))
|
|
185
|
+
y << message
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# Unary: the encoded response. Streaming: nil, after pushing each
|
|
191
|
+
# message through the sender.
|
|
192
|
+
def respond(result, spec, sender)
|
|
193
|
+
unless spec[:response_stream]
|
|
194
|
+
encoded = encode_response(result, spec)
|
|
195
|
+
send_response_headers
|
|
196
|
+
return encoded
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
unless result.respond_to?(:each)
|
|
200
|
+
raise TypeError, "streaming handler #{self.class}##{spec[:handler]} must return an Enumerable, got #{result.class}"
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
result.each { |item| push_response(sender, encode_response(item, spec)) }
|
|
204
|
+
nil
|
|
205
|
+
end
|
|
206
|
+
|
|
179
207
|
def read_request(reader, spec)
|
|
180
208
|
bytes = reader.next
|
|
181
209
|
if bytes.nil?
|
|
@@ -205,11 +233,30 @@ module Tonic
|
|
|
205
233
|
end
|
|
206
234
|
|
|
207
235
|
def push_response(sender, bytes)
|
|
236
|
+
send_response_headers
|
|
208
237
|
sender.push(bytes)
|
|
209
238
|
rescue IOError
|
|
210
239
|
# The client went away; nothing left to deliver to.
|
|
211
240
|
raise RpcError.new("client cancelled the stream", code: :cancelled)
|
|
212
241
|
end
|
|
242
|
+
|
|
243
|
+
def send_response_headers
|
|
244
|
+
return if @grpc_headers_sent
|
|
245
|
+
|
|
246
|
+
@grpc_headers_sent = true
|
|
247
|
+
return if @response_metadata.nil? || @response_metadata.empty? || @grpc_sender.nil?
|
|
248
|
+
|
|
249
|
+
@grpc_sender.send_headers(Metadata.encode(@response_metadata))
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
# Runs in `ensure`, so it must never raise over the call's outcome.
|
|
253
|
+
def attach_trailing_metadata
|
|
254
|
+
return if @trailing_metadata.nil? || @trailing_metadata.empty? || @grpc_sender.nil?
|
|
255
|
+
|
|
256
|
+
@grpc_sender.add_trailers(Metadata.encode(@trailing_metadata))
|
|
257
|
+
rescue StandardError => e
|
|
258
|
+
Tonic::Rails.log_exception(e, "#{self.class} trailing metadata")
|
|
259
|
+
end
|
|
213
260
|
end
|
|
214
261
|
end
|
|
215
262
|
end
|
|
@@ -47,7 +47,8 @@ module Tonic
|
|
|
47
47
|
# Concurrent HTTP/2 streams per connection. 0 = unlimited. Default 32.
|
|
48
48
|
attr_accessor :concurrency_limit_per_connection
|
|
49
49
|
|
|
50
|
-
# Log
|
|
50
|
+
# Log one line per RPC (path, status, duration) through `logger`.
|
|
51
|
+
# Unexpected exceptions are always logged. Default false.
|
|
51
52
|
attr_accessor :log_requests
|
|
52
53
|
|
|
53
54
|
# Callables wrapping every RPC, outermost first:
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_support"
|
|
4
|
+
require "active_support/notifications"
|
|
5
|
+
|
|
6
|
+
module Tonic
|
|
7
|
+
module Rails
|
|
8
|
+
# Every RPC is wrapped in an ActiveSupport::Notifications event, so APM
|
|
9
|
+
# agents, OpenTelemetry and your own subscribers can observe it:
|
|
10
|
+
#
|
|
11
|
+
# ActiveSupport::Notifications.subscribe("process_rpc.tonic_rails") do |event|
|
|
12
|
+
# event.payload[:path] # "/demo.v1.ItemService/GetItem"
|
|
13
|
+
# event.payload[:service] # "demo.v1.ItemService"
|
|
14
|
+
# event.payload[:rpc] # "GetItem"
|
|
15
|
+
# event.payload[:metadata] # request metadata Hash
|
|
16
|
+
# event.payload[:code] # gRPC status code, 0 = OK
|
|
17
|
+
# event.payload[:status] # :ok, :not_found, :internal, ...
|
|
18
|
+
# event.duration # milliseconds
|
|
19
|
+
# end
|
|
20
|
+
#
|
|
21
|
+
# When a call fails, :exception_object holds the RpcError. If a handler
|
|
22
|
+
# raised something unexpected, that RpcError's #cause is the original
|
|
23
|
+
# exception.
|
|
24
|
+
module Instrumentation
|
|
25
|
+
EVENT = "process_rpc.tonic_rails"
|
|
26
|
+
|
|
27
|
+
module_function
|
|
28
|
+
|
|
29
|
+
# @api private
|
|
30
|
+
def instrument(path, metadata)
|
|
31
|
+
# Without subscribers, skip building the payload: this runs on
|
|
32
|
+
# every RPC.
|
|
33
|
+
return yield unless ActiveSupport::Notifications.notifier.listening?(EVENT)
|
|
34
|
+
|
|
35
|
+
service, rpc = path.delete_prefix("/").split("/", 2)
|
|
36
|
+
payload = { path: path, service: service, rpc: rpc, metadata: metadata, code: nil, status: nil }
|
|
37
|
+
ActiveSupport::Notifications.instrument(EVENT, payload) do
|
|
38
|
+
result = yield
|
|
39
|
+
payload[:code] = 0
|
|
40
|
+
payload[:status] = :ok
|
|
41
|
+
result
|
|
42
|
+
rescue RpcError => e
|
|
43
|
+
payload[:code] = e.code
|
|
44
|
+
payload[:status] = e.code_name
|
|
45
|
+
raise
|
|
46
|
+
rescue StandardError
|
|
47
|
+
payload[:code] = RpcError::CODES[:internal]
|
|
48
|
+
payload[:status] = :internal
|
|
49
|
+
raise
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Logs one line per RPC through the configured logger, e.g.
|
|
54
|
+
# gRPC /demo.v1.ItemService/GetItem OK in 1.4ms
|
|
55
|
+
# gRPC /demo.v1.ItemService/GetItem NOT_FOUND in 0.9ms: item 7 not found
|
|
56
|
+
# Enabled by `config.log_requests = true`.
|
|
57
|
+
class RequestLogger
|
|
58
|
+
def initialize(logger)
|
|
59
|
+
@logger = logger
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def call(event)
|
|
63
|
+
payload = event.payload
|
|
64
|
+
status = (payload[:status] || :unknown).to_s.upcase
|
|
65
|
+
line = format("gRPC %<path>s %<status>s in %<ms>.1fms", path: payload[:path], status: status, ms: event.duration)
|
|
66
|
+
error = payload[:exception_object]
|
|
67
|
+
line = "#{line}: #{error.message}" if error && payload[:code] != 0
|
|
68
|
+
@logger.info(line)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# @return [Object] the subscription, for #unsubscribe
|
|
72
|
+
def subscribe
|
|
73
|
+
ActiveSupport::Notifications.subscribe(EVENT, self)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tonic
|
|
4
|
+
module Rails
|
|
5
|
+
# Converts handler-supplied gRPC metadata into header pairs.
|
|
6
|
+
#
|
|
7
|
+
# Names are lower-cased and must be valid gRPC metadata names; names
|
|
8
|
+
# ending in "-bin" carry binary values, which are base64-encoded as the
|
|
9
|
+
# gRPC spec requires. Other values must be printable ASCII.
|
|
10
|
+
module Metadata
|
|
11
|
+
NAME = /\A[0-9a-z_.-]+\z/
|
|
12
|
+
PRINTABLE = /\A[\x20-\x7e]*\z/
|
|
13
|
+
RESERVED = %w[content-type te user-agent content-length connection transfer-encoding].freeze
|
|
14
|
+
|
|
15
|
+
module_function
|
|
16
|
+
|
|
17
|
+
# @param metadata [Hash{String,Symbol=>String,Array<String>}]
|
|
18
|
+
# @return [Hash{String=>String}]
|
|
19
|
+
# @raise [ArgumentError] for invalid or reserved names and values
|
|
20
|
+
def encode(metadata)
|
|
21
|
+
metadata.each_with_object({}) do |(name, value), out|
|
|
22
|
+
name = name.to_s.downcase
|
|
23
|
+
validate_name!(name)
|
|
24
|
+
out[name] = encode_value(name, value)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def validate_name!(name)
|
|
29
|
+
raise ArgumentError, "invalid gRPC metadata name #{name.inspect}" unless NAME.match?(name)
|
|
30
|
+
return unless RESERVED.include?(name) || (name.start_with?("grpc-") && name != "grpc-status-details-bin")
|
|
31
|
+
|
|
32
|
+
raise ArgumentError, "gRPC metadata name #{name.inspect} is reserved"
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def encode_value(name, value)
|
|
36
|
+
values = Array(value).map(&:to_s)
|
|
37
|
+
if name.end_with?("-bin")
|
|
38
|
+
values.map { |v| [v].pack("m0") }.join(",")
|
|
39
|
+
else
|
|
40
|
+
values.each do |v|
|
|
41
|
+
next if PRINTABLE.match?(v)
|
|
42
|
+
|
|
43
|
+
raise ArgumentError, "gRPC metadata #{name} must be printable ASCII; use a -bin name for binary"
|
|
44
|
+
end
|
|
45
|
+
values.join(",")
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
data/lib/tonic/rails/railtie.rb
CHANGED
|
@@ -28,8 +28,8 @@ module Tonic
|
|
|
28
28
|
config.after_initialize do
|
|
29
29
|
configuration = Tonic::Rails.configuration
|
|
30
30
|
# Only inside an actual web server process — not consoles, rake
|
|
31
|
-
# tasks, or test runs.
|
|
32
|
-
#
|
|
31
|
+
# tasks, or test runs. With Puma, prefer `plugin :tonic_rails`
|
|
32
|
+
# (lib/puma/plugin/tonic_rails.rb), which also covers cluster mode.
|
|
33
33
|
if configuration.auto_start && defined?(::Rails::Server)
|
|
34
34
|
server = Tonic::Rails.server
|
|
35
35
|
thread = Thread.new do
|
|
@@ -2,13 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
module Tonic
|
|
4
4
|
module Rails
|
|
5
|
-
# Raise from a handler to end the RPC with a gRPC status.
|
|
5
|
+
# Raise from a handler (or interceptor) to end the RPC with a gRPC status.
|
|
6
6
|
#
|
|
7
|
-
# raise Tonic::Rails::RpcError.new("
|
|
7
|
+
# raise Tonic::Rails::RpcError.new("item not found", code: :not_found)
|
|
8
8
|
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
9
|
+
# Optionally attach trailing metadata and rich error details (protobuf
|
|
10
|
+
# messages such as google.rpc.BadRequest, sent in the standard
|
|
11
|
+
# grpc-status-details-bin trailer as a google.rpc.Status):
|
|
12
|
+
#
|
|
13
|
+
# raise Tonic::Rails::RpcError.new(
|
|
14
|
+
# "invalid item", code: :invalid_argument,
|
|
15
|
+
# metadata: { "x-request-id" => request_id },
|
|
16
|
+
# details: [Google::Rpc::BadRequest.new(field_violations: [...])],
|
|
17
|
+
# )
|
|
18
|
+
#
|
|
19
|
+
# The message, metadata and details go to the client, so keep internal
|
|
20
|
+
# details out of them. Any other exception becomes INTERNAL with a
|
|
21
|
+
# generic message and is logged server-side.
|
|
12
22
|
class RpcError < Error
|
|
13
23
|
CODES = {
|
|
14
24
|
ok: 0,
|
|
@@ -35,13 +45,70 @@ module Tonic
|
|
|
35
45
|
# @return [Integer] numeric gRPC status code
|
|
36
46
|
attr_reader :code
|
|
37
47
|
|
|
48
|
+
# @return [Hash] trailing metadata sent with the status
|
|
49
|
+
attr_reader :metadata
|
|
50
|
+
|
|
51
|
+
# @return [Array<Google::Protobuf::AbstractMessage>] rich error details
|
|
52
|
+
attr_reader :details
|
|
53
|
+
|
|
38
54
|
# @param message [String] status message sent to the client
|
|
39
55
|
# @param code [Symbol, Integer] gRPC status code (see CODES)
|
|
40
|
-
|
|
56
|
+
# @param metadata [Hash] trailing metadata (see Tonic::Rails::Metadata)
|
|
57
|
+
# @param details [Array<Google::Protobuf::AbstractMessage>] messages
|
|
58
|
+
# packed into google.rpc.Status.details
|
|
59
|
+
def initialize(message = nil, code: :unknown, metadata: {}, details: [])
|
|
41
60
|
@code = self.class.normalize_code(code)
|
|
61
|
+
@metadata = metadata
|
|
62
|
+
@details = Array(details)
|
|
42
63
|
super(message || code_name.to_s)
|
|
43
64
|
end
|
|
44
65
|
|
|
66
|
+
# @api private
|
|
67
|
+
# @return [Hash{String=>String}] trailers to send with this status
|
|
68
|
+
def trailers
|
|
69
|
+
trailers = Metadata.encode(metadata)
|
|
70
|
+
trailers["grpc-status-details-bin"] = [status_proto].pack("m0") unless details.empty?
|
|
71
|
+
trailers
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# The encoded google.rpc.Status (code = 1, message = 2, details = 3 as
|
|
75
|
+
# google.protobuf.Any), built directly so googleapis protos aren't a
|
|
76
|
+
# dependency.
|
|
77
|
+
def status_proto
|
|
78
|
+
require "google/protobuf/well_known_types"
|
|
79
|
+
buf = (+"").b
|
|
80
|
+
buf << field_key(1, 0) << varint(code)
|
|
81
|
+
buf << length_delimited(2, message.b)
|
|
82
|
+
details.each do |detail|
|
|
83
|
+
any = detail.is_a?(Google::Protobuf::Any) ? detail : Google::Protobuf::Any.pack(detail)
|
|
84
|
+
buf << length_delimited(3, Google::Protobuf::Any.encode(any))
|
|
85
|
+
end
|
|
86
|
+
buf
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
private
|
|
90
|
+
|
|
91
|
+
def field_key(field, wire_type)
|
|
92
|
+
varint((field << 3) | wire_type)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def length_delimited(field, bytes)
|
|
96
|
+
field_key(field, 2) + varint(bytes.bytesize) + bytes
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def varint(value)
|
|
100
|
+
bytes = []
|
|
101
|
+
loop do
|
|
102
|
+
byte = value & 0x7f
|
|
103
|
+
value >>= 7
|
|
104
|
+
bytes << (value.zero? ? byte : byte | 0x80)
|
|
105
|
+
break if value.zero?
|
|
106
|
+
end
|
|
107
|
+
bytes.pack("C*")
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
public
|
|
111
|
+
|
|
45
112
|
# @return [Symbol] e.g. :not_found
|
|
46
113
|
def code_name
|
|
47
114
|
CODE_NAMES.fetch(@code)
|
data/lib/tonic/rails/server.rb
CHANGED
|
@@ -20,8 +20,7 @@ module Tonic
|
|
|
20
20
|
@router = Router.new
|
|
21
21
|
@native = nil
|
|
22
22
|
@state = :stopped
|
|
23
|
-
@
|
|
24
|
-
@stop_writer = nil
|
|
23
|
+
@stop_signal = nil
|
|
25
24
|
@stop_requested = false
|
|
26
25
|
@lock = Mutex.new
|
|
27
26
|
end
|
|
@@ -51,12 +50,17 @@ module Tonic
|
|
|
51
50
|
@configuration.validate!
|
|
52
51
|
Tonic::Rails.native_extension!
|
|
53
52
|
Tonic::Rails._set_shutting_down(false)
|
|
54
|
-
|
|
53
|
+
# A Queue rather than a self-pipe: pushing to it is safe from trap
|
|
54
|
+
# handlers, and unlike pipes it can be waited on with a timeout
|
|
55
|
+
# on every platform (Windows select() only handles sockets).
|
|
56
|
+
@stop_signal = Thread::Queue.new
|
|
55
57
|
stop if @stop_requested # requested while we were starting up
|
|
56
58
|
@native = build_native_server
|
|
57
59
|
@native.start_server
|
|
60
|
+
log_pump = spawn_log_pump
|
|
58
61
|
@state = :running
|
|
59
62
|
|
|
63
|
+
request_log = subscribe_request_logger
|
|
60
64
|
previous_handlers = install_signal_handlers ? trap_signals : {}
|
|
61
65
|
workers = Array.new(@configuration.dispatch_threads) { |i| spawn_worker(i) }
|
|
62
66
|
log_startup
|
|
@@ -65,11 +69,12 @@ module Tonic
|
|
|
65
69
|
wait_for_stop(workers)
|
|
66
70
|
shutdown(workers)
|
|
67
71
|
ensure
|
|
72
|
+
ActiveSupport::Notifications.unsubscribe(request_log) if request_log
|
|
68
73
|
previous_handlers&.each { |signal, handler| Signal.trap(signal, handler || "DEFAULT") }
|
|
69
74
|
@native&.stop_server
|
|
75
|
+
stop_log_pump(log_pump)
|
|
70
76
|
@native = nil
|
|
71
|
-
|
|
72
|
-
@stop_reader = @stop_writer = nil
|
|
77
|
+
@stop_signal = nil
|
|
73
78
|
Tonic::Rails._set_shutting_down(false)
|
|
74
79
|
@state = :stopped
|
|
75
80
|
end
|
|
@@ -79,9 +84,7 @@ module Tonic
|
|
|
79
84
|
# other threads; returns immediately.
|
|
80
85
|
def stop
|
|
81
86
|
@stop_requested = true
|
|
82
|
-
@
|
|
83
|
-
rescue IOError
|
|
84
|
-
# Already stopped.
|
|
87
|
+
@stop_signal&.push(:stop)
|
|
85
88
|
end
|
|
86
89
|
|
|
87
90
|
def running?
|
|
@@ -113,7 +116,6 @@ module Tonic
|
|
|
113
116
|
native.set_worker_threads(config.worker_threads || 0)
|
|
114
117
|
native.set_keepalive(config.keepalive_interval.to_f, config.keepalive_timeout.to_f)
|
|
115
118
|
native.set_dispatch_queue_size(config.dispatch_queue_size)
|
|
116
|
-
native.set_log_requests(config.log_requests ? true : false)
|
|
117
119
|
native.set_compression(config.compression ? true : false)
|
|
118
120
|
native.set_reflection(config.reflection ? true : false)
|
|
119
121
|
native.set_reflection_descriptors(Reflection.file_descriptors(@router.service_names)) if config.reflection
|
|
@@ -133,8 +135,9 @@ module Tonic
|
|
|
133
135
|
end
|
|
134
136
|
|
|
135
137
|
guarded = lambda do |path, metadata, transport|
|
|
136
|
-
call.call(path, metadata, transport)
|
|
137
|
-
rescue RpcError
|
|
138
|
+
Instrumentation.instrument(path, metadata) { call.call(path, metadata, transport) }
|
|
139
|
+
rescue RpcError => e
|
|
140
|
+
attach_error_trailers(transport, e)
|
|
138
141
|
raise
|
|
139
142
|
rescue StandardError => e
|
|
140
143
|
Tonic::Rails.log_exception(e, path)
|
|
@@ -144,6 +147,15 @@ module Tonic
|
|
|
144
147
|
wrap_with_rails_executor(guarded)
|
|
145
148
|
end
|
|
146
149
|
|
|
150
|
+
# An RpcError's own metadata and rich details travel as trailers.
|
|
151
|
+
def attach_error_trailers(transport, error)
|
|
152
|
+
return if error.metadata.empty? && error.details.empty?
|
|
153
|
+
|
|
154
|
+
transport[:sender]&.add_trailers(error.trailers)
|
|
155
|
+
rescue StandardError => e
|
|
156
|
+
Tonic::Rails.log_exception(e, "RpcError trailers")
|
|
157
|
+
end
|
|
158
|
+
|
|
147
159
|
# Each RPC runs inside the Rails executor (or reloader, when code
|
|
148
160
|
# reloading is on): ActiveRecord connections are returned, query
|
|
149
161
|
# caches and CurrentAttributes are reset — the same isolation Rack
|
|
@@ -156,6 +168,41 @@ module Tonic
|
|
|
156
168
|
->(path, metadata, transport) { wrapper.wrap { callback.call(path, metadata, transport) } }
|
|
157
169
|
end
|
|
158
170
|
|
|
171
|
+
def subscribe_request_logger
|
|
172
|
+
return unless @configuration.log_requests && @configuration.logger
|
|
173
|
+
|
|
174
|
+
Instrumentation::RequestLogger.new(@configuration.logger).subscribe
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# Transport events from the Rust side (rejected calls, expired
|
|
178
|
+
# deadlines, refused connections, faults) can't call Ruby directly;
|
|
179
|
+
# this thread drains them into the configured logger.
|
|
180
|
+
def spawn_log_pump
|
|
181
|
+
native = @native
|
|
182
|
+
logger = @configuration.logger
|
|
183
|
+
@log_pump_running = true
|
|
184
|
+
Thread.new do
|
|
185
|
+
Thread.current.name = "tonic-rails-log"
|
|
186
|
+
write_transport_logs(logger, native.next_log_events(0.25)) while @log_pump_running
|
|
187
|
+
write_transport_logs(logger, native.next_log_events(0)) # whatever shutdown produced
|
|
188
|
+
rescue StandardError => e
|
|
189
|
+
warn("tonic-rails: log thread stopped: #{e.class}: #{e.message}")
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def stop_log_pump(thread)
|
|
194
|
+
return unless thread
|
|
195
|
+
|
|
196
|
+
@log_pump_running = false
|
|
197
|
+
thread.join(2)
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def write_transport_logs(logger, events)
|
|
201
|
+
return unless logger
|
|
202
|
+
|
|
203
|
+
events.each { |level, message| logger.public_send(level, "tonic-rails: #{message}") }
|
|
204
|
+
end
|
|
205
|
+
|
|
159
206
|
def spawn_worker(index)
|
|
160
207
|
native = @native
|
|
161
208
|
Thread.new do
|
|
@@ -174,7 +221,7 @@ module Tonic
|
|
|
174
221
|
|
|
175
222
|
def wait_for_stop(workers)
|
|
176
223
|
loop do
|
|
177
|
-
break if @
|
|
224
|
+
break if @stop_signal.pop(timeout: 1)
|
|
178
225
|
|
|
179
226
|
dead = workers.reject(&:alive?)
|
|
180
227
|
unless dead.empty?
|
data/lib/tonic/rails/testing.rb
CHANGED
|
@@ -28,19 +28,35 @@ module Tonic
|
|
|
28
28
|
attr_reader :error
|
|
29
29
|
end
|
|
30
30
|
|
|
31
|
-
# Response side of a call: collects encoded messages.
|
|
31
|
+
# Response side of a call: collects encoded messages and metadata.
|
|
32
32
|
class Sender
|
|
33
|
-
attr_reader :messages
|
|
33
|
+
attr_reader :messages, :headers, :trailers
|
|
34
34
|
|
|
35
35
|
def initialize
|
|
36
36
|
@messages = []
|
|
37
|
+
@headers = {}
|
|
38
|
+
@trailers = {}
|
|
37
39
|
end
|
|
38
40
|
|
|
39
41
|
def push(bytes)
|
|
40
42
|
@messages << bytes
|
|
41
43
|
end
|
|
44
|
+
|
|
45
|
+
def send_headers(headers)
|
|
46
|
+
raise "response headers must be sent before any response message" unless @messages.empty?
|
|
47
|
+
|
|
48
|
+
@headers.merge!(headers)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def add_trailers(trailers)
|
|
52
|
+
@trailers.merge!(trailers)
|
|
53
|
+
end
|
|
42
54
|
end
|
|
43
55
|
|
|
56
|
+
# What a call produced: the response (or responses, for streaming),
|
|
57
|
+
# plus the response headers and trailers as they'd go on the wire.
|
|
58
|
+
Result = Struct.new(:response, :headers, :trailers)
|
|
59
|
+
|
|
44
60
|
module_function
|
|
45
61
|
|
|
46
62
|
# @param service_class [Class] handler class
|
|
@@ -53,6 +69,18 @@ module Tonic
|
|
|
53
69
|
# an Array of responses for server- and bidi-streaming RPCs
|
|
54
70
|
# @raise [RpcError] when the handler ends the call with a status
|
|
55
71
|
def call(service_class, rpc_name, request, metadata: {}, timeout_ms: nil)
|
|
72
|
+
invoke(service_class, rpc_name, request, metadata: metadata, timeout_ms: timeout_ms).response
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Like #call, but returns a Result with the response metadata too:
|
|
76
|
+
#
|
|
77
|
+
# result = Tonic::Rails::Testing.invoke(ItemService, :GetItem, { id: 1 })
|
|
78
|
+
# result.response.name # => "Widget"
|
|
79
|
+
# result.headers["x-cache"] # => "hit"
|
|
80
|
+
# result.trailers["x-cost-ms"] # => "3"
|
|
81
|
+
#
|
|
82
|
+
# @return [Result]
|
|
83
|
+
def invoke(service_class, rpc_name, request, metadata: {}, timeout_ms: nil)
|
|
56
84
|
spec = service_class.grpc_rpcs.fetch(rpc_name.to_s) do
|
|
57
85
|
raise ArgumentError, "#{service_class} has no RPC #{rpc_name}"
|
|
58
86
|
end
|
|
@@ -69,11 +97,12 @@ module Tonic
|
|
|
69
97
|
timeout_ms: timeout_ms,
|
|
70
98
|
)
|
|
71
99
|
|
|
72
|
-
if spec[:response_stream]
|
|
100
|
+
response = if spec[:response_stream]
|
|
73
101
|
sender.messages.map { |bytes| spec[:response].decode(bytes) }
|
|
74
102
|
else
|
|
75
103
|
spec[:response].decode(result)
|
|
76
104
|
end
|
|
105
|
+
Result.new(response, sender.headers, sender.trailers)
|
|
77
106
|
end
|
|
78
107
|
|
|
79
108
|
def encode(klass, message)
|
data/lib/tonic/rails/version.rb
CHANGED
data/lib/tonic-rails.rb
CHANGED
|
@@ -10,9 +10,11 @@ module Tonic
|
|
|
10
10
|
end
|
|
11
11
|
|
|
12
12
|
require "tonic/rails/version"
|
|
13
|
+
require "tonic/rails/metadata"
|
|
13
14
|
require "tonic/rails/rpc_error"
|
|
14
15
|
require "tonic/rails/configuration"
|
|
15
16
|
require "tonic/rails/router"
|
|
17
|
+
require "tonic/rails/instrumentation"
|
|
16
18
|
require "tonic/rails/base"
|
|
17
19
|
require "tonic/rails/reflection"
|
|
18
20
|
require "tonic/rails/server"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: tonic-rails
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: aarch64-linux-gnu
|
|
6
6
|
authors:
|
|
7
7
|
- Matt Tanous
|
|
@@ -54,12 +54,15 @@ files:
|
|
|
54
54
|
- CHANGELOG.md
|
|
55
55
|
- LICENSE
|
|
56
56
|
- README.md
|
|
57
|
+
- lib/puma/plugin/tonic_rails.rb
|
|
57
58
|
- lib/tonic-rails.rb
|
|
58
59
|
- lib/tonic/rails/3.3/tonic_rails_native.so
|
|
59
60
|
- lib/tonic/rails/3.4/tonic_rails_native.so
|
|
60
61
|
- lib/tonic/rails/4.0/tonic_rails_native.so
|
|
61
62
|
- lib/tonic/rails/base.rb
|
|
62
63
|
- lib/tonic/rails/configuration.rb
|
|
64
|
+
- lib/tonic/rails/instrumentation.rb
|
|
65
|
+
- lib/tonic/rails/metadata.rb
|
|
63
66
|
- lib/tonic/rails/railtie.rb
|
|
64
67
|
- lib/tonic/rails/reflection.rb
|
|
65
68
|
- lib/tonic/rails/router.rb
|