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
|
@@ -24,17 +24,32 @@ module Tonic
|
|
|
24
24
|
module Instrumentation
|
|
25
25
|
EVENT = "process_rpc.tonic_rails"
|
|
26
26
|
|
|
27
|
+
# Outgoing calls made with Tonic::Rails::Client publish
|
|
28
|
+
# "request_rpc.tonic_rails", with the same payload plus :address,
|
|
29
|
+
# the server called.
|
|
30
|
+
REQUEST_EVENT = "request_rpc.tonic_rails"
|
|
31
|
+
|
|
27
32
|
module_function
|
|
28
33
|
|
|
29
34
|
# @api private
|
|
30
|
-
def instrument(path, metadata)
|
|
35
|
+
def instrument(path, metadata, &)
|
|
36
|
+
publish(EVENT, path, metadata, &)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# @api private
|
|
40
|
+
def instrument_request(path, address, metadata, &)
|
|
41
|
+
publish(REQUEST_EVENT, path, metadata, address: address, &)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# @api private
|
|
45
|
+
def publish(event, path, metadata, **extra)
|
|
31
46
|
# Without subscribers, skip building the payload: this runs on
|
|
32
47
|
# every RPC.
|
|
33
|
-
return yield unless ActiveSupport::Notifications.notifier.listening?(
|
|
48
|
+
return yield unless ActiveSupport::Notifications.notifier.listening?(event)
|
|
34
49
|
|
|
35
50
|
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(
|
|
51
|
+
payload = { path: path, service: service, rpc: rpc, metadata: metadata, code: nil, status: nil, **extra }
|
|
52
|
+
ActiveSupport::Notifications.instrument(event, payload) do
|
|
38
53
|
result = yield
|
|
39
54
|
payload[:code] = 0
|
|
40
55
|
payload[:status] = :ok
|
data/lib/tonic/rails/railtie.rb
CHANGED
|
@@ -31,6 +31,11 @@ module Tonic
|
|
|
31
31
|
# tasks, or test runs. With Puma, prefer `plugin :tonic_rails`
|
|
32
32
|
# (lib/puma/plugin/tonic_rails.rb), which also covers cluster mode.
|
|
33
33
|
if configuration.auto_start && defined?(::Rails::Server)
|
|
34
|
+
if configuration.multi_process?
|
|
35
|
+
raise Error, "auto_start can't run multiple processes (processes = #{configuration.processes}); " \
|
|
36
|
+
"use `plugin :tonic_rails` in config/puma.rb or `bin/rails tonic_rails:serve`"
|
|
37
|
+
end
|
|
38
|
+
|
|
34
39
|
server = Tonic::Rails.server
|
|
35
40
|
thread = Thread.new do
|
|
36
41
|
Thread.current.name = "tonic-rails"
|
|
@@ -19,6 +19,8 @@ module Tonic
|
|
|
19
19
|
# The message, metadata and details go to the client, so keep internal
|
|
20
20
|
# details out of them. Any other exception becomes INTERNAL with a
|
|
21
21
|
# generic message and is logged server-side.
|
|
22
|
+
#
|
|
23
|
+
# Tonic::Rails::Client raises it too, for calls that fail.
|
|
22
24
|
class RpcError < Error
|
|
23
25
|
CODES = {
|
|
24
26
|
ok: 0,
|
|
@@ -114,6 +116,65 @@ module Tonic
|
|
|
114
116
|
CODE_NAMES.fetch(@code)
|
|
115
117
|
end
|
|
116
118
|
|
|
119
|
+
# Unpacks the details of an encoded google.rpc.Status (from the
|
|
120
|
+
# grpc-status-details-bin trailer). Details whose message type is
|
|
121
|
+
# loaded come back as that message; others stay Google::Protobuf::Any.
|
|
122
|
+
# @api private
|
|
123
|
+
# @return [Array<Google::Protobuf::AbstractMessage>]
|
|
124
|
+
def self.decode_details(bytes)
|
|
125
|
+
return [] if bytes.nil? || bytes.empty?
|
|
126
|
+
|
|
127
|
+
require "google/protobuf/well_known_types"
|
|
128
|
+
details = []
|
|
129
|
+
each_length_delimited_field(bytes.b) do |field, value|
|
|
130
|
+
details << unpack_detail(Google::Protobuf::Any.decode(value)) if field == 3
|
|
131
|
+
end
|
|
132
|
+
details
|
|
133
|
+
rescue Google::Protobuf::ParseError, ArgumentError
|
|
134
|
+
[]
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Yields (field number, bytes) for each length-delimited field,
|
|
138
|
+
# skipping varints; raises ArgumentError on anything malformed.
|
|
139
|
+
def self.each_length_delimited_field(buf)
|
|
140
|
+
pos = 0
|
|
141
|
+
while pos < buf.bytesize
|
|
142
|
+
key, pos = read_varint(buf, pos)
|
|
143
|
+
case key & 7
|
|
144
|
+
when 0 then _, pos = read_varint(buf, pos)
|
|
145
|
+
when 2
|
|
146
|
+
length, pos = read_varint(buf, pos)
|
|
147
|
+
raise ArgumentError, "truncated google.rpc.Status" if pos + length > buf.bytesize
|
|
148
|
+
|
|
149
|
+
yield key >> 3, buf.byteslice(pos, length)
|
|
150
|
+
pos += length
|
|
151
|
+
else raise ArgumentError, "unexpected wire type in google.rpc.Status"
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
private_class_method :each_length_delimited_field
|
|
156
|
+
|
|
157
|
+
def self.read_varint(buf, pos)
|
|
158
|
+
value = 0
|
|
159
|
+
shift = 0
|
|
160
|
+
loop do
|
|
161
|
+
byte = buf.getbyte(pos) or raise ArgumentError, "truncated varint"
|
|
162
|
+
pos += 1
|
|
163
|
+
value |= (byte & 0x7f) << shift
|
|
164
|
+
return [value, pos] if byte < 0x80
|
|
165
|
+
|
|
166
|
+
shift += 7
|
|
167
|
+
raise ArgumentError, "varint too long" if shift > 63
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
private_class_method :read_varint
|
|
171
|
+
|
|
172
|
+
def self.unpack_detail(any)
|
|
173
|
+
type = Google::Protobuf::DescriptorPool.generated_pool.lookup(any.type_name)
|
|
174
|
+
type.is_a?(Google::Protobuf::Descriptor) ? any.unpack(type.msgclass) : any
|
|
175
|
+
end
|
|
176
|
+
private_class_method :unpack_detail
|
|
177
|
+
|
|
117
178
|
# @api private
|
|
118
179
|
def self.normalize_code(code)
|
|
119
180
|
case code
|
data/lib/tonic/rails/server.rb
CHANGED
|
@@ -36,8 +36,9 @@ module Tonic
|
|
|
36
36
|
# Start serving and block until stopped.
|
|
37
37
|
# @param install_signal_handlers [Boolean] trap TERM and INT to stop.
|
|
38
38
|
# Pass false when embedding in a process that owns its signals.
|
|
39
|
-
# @yield once the server is accepting connections
|
|
40
|
-
|
|
39
|
+
# @yield once the server is accepting connections (with
|
|
40
|
+
# `processes > 1`, once every worker is)
|
|
41
|
+
def start(install_signal_handlers: true, &)
|
|
41
42
|
@lock.synchronize do
|
|
42
43
|
raise Error, "server is already running" unless @state == :stopped
|
|
43
44
|
|
|
@@ -50,40 +51,31 @@ module Tonic
|
|
|
50
51
|
|
|
51
52
|
@configuration.validate!
|
|
52
53
|
Tonic::Rails.native_extension!
|
|
53
|
-
Tonic::Rails._set_shutting_down(false)
|
|
54
54
|
# A Queue rather than a self-pipe: pushing to it is safe from trap
|
|
55
55
|
# handlers, and unlike pipes it can be waited on with a timeout
|
|
56
56
|
# on every platform (Windows select() only handles sockets).
|
|
57
57
|
@stop_signal = Thread::Queue.new
|
|
58
58
|
stop if @stop_requested # requested while we were starting up
|
|
59
|
-
@
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
request_log = subscribe_request_logger
|
|
66
|
-
previous_handlers = install_signal_handlers ? trap_signals : {}
|
|
67
|
-
workers = Array.new(@configuration.dispatch_threads) { |i| spawn_worker(i) }
|
|
68
|
-
log_startup
|
|
69
|
-
yield self if block_given?
|
|
70
|
-
|
|
71
|
-
wait_for_stop(workers)
|
|
72
|
-
@health.detach
|
|
73
|
-
shutdown(workers)
|
|
59
|
+
if @configuration.multi_process?
|
|
60
|
+
run_cluster(install_signal_handlers, &)
|
|
61
|
+
else
|
|
62
|
+
serve(install_signal_handlers: install_signal_handlers, &)
|
|
63
|
+
end
|
|
74
64
|
ensure
|
|
75
|
-
ActiveSupport::Notifications.unsubscribe(request_log) if request_log
|
|
76
|
-
previous_handlers&.each { |signal, handler| Signal.trap(signal, handler || "DEFAULT") }
|
|
77
|
-
@health.detach
|
|
78
|
-
@native&.stop_server
|
|
79
|
-
stop_log_pump(log_pump)
|
|
80
|
-
@native = nil
|
|
81
65
|
@stop_signal = nil
|
|
82
|
-
Tonic::Rails._set_shutting_down(false)
|
|
83
66
|
@state = :stopped
|
|
84
67
|
end
|
|
85
68
|
end
|
|
86
69
|
|
|
70
|
+
# @api private
|
|
71
|
+
# The body of a worker process in multi-process mode (see Cluster):
|
|
72
|
+
# serve on the listening socket the parent bound.
|
|
73
|
+
def run_worker(listener, index, &)
|
|
74
|
+
@worker_index = index
|
|
75
|
+
@configuration.on_worker_boot&.call(index)
|
|
76
|
+
serve(install_signal_handlers: true, listener: listener, &)
|
|
77
|
+
end
|
|
78
|
+
|
|
87
79
|
# Request a graceful stop. Safe to call from signal handlers and
|
|
88
80
|
# other threads; returns immediately.
|
|
89
81
|
def stop
|
|
@@ -125,6 +117,55 @@ module Tonic
|
|
|
125
117
|
|
|
126
118
|
private
|
|
127
119
|
|
|
120
|
+
def run_cluster(install_signal_handlers)
|
|
121
|
+
@state = :running
|
|
122
|
+
Cluster.new(self, @configuration, @stop_signal).run(install_signal_handlers: install_signal_handlers) do
|
|
123
|
+
log_startup
|
|
124
|
+
yield self if block_given?
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def serve(install_signal_handlers:, listener: nil)
|
|
129
|
+
Tonic::Rails._set_shutting_down(false)
|
|
130
|
+
@native = build_native_server
|
|
131
|
+
@native.set_listener_fd(listener.fileno) if listener
|
|
132
|
+
@native.start_server
|
|
133
|
+
# The transport holds its own copy now; let go of ours, so the
|
|
134
|
+
# socket closes once every worker has stopped accepting.
|
|
135
|
+
listener&.close
|
|
136
|
+
log_pump = spawn_log_pump
|
|
137
|
+
@health.attach(@native)
|
|
138
|
+
@state = :running
|
|
139
|
+
|
|
140
|
+
request_log = subscribe_request_logger
|
|
141
|
+
previous_handlers = install_signal_handlers ? trap_signals : {}
|
|
142
|
+
workers = Array.new(@configuration.dispatch_threads) { |i| spawn_worker(i) }
|
|
143
|
+
log_startup unless worker?
|
|
144
|
+
yield self if block_given?
|
|
145
|
+
|
|
146
|
+
wait_for_stop(workers)
|
|
147
|
+
@health.detach
|
|
148
|
+
shutdown(workers)
|
|
149
|
+
ensure
|
|
150
|
+
ActiveSupport::Notifications.unsubscribe(request_log) if request_log
|
|
151
|
+
previous_handlers&.each { |signal, handler| Signal.trap(signal, handler || "DEFAULT") }
|
|
152
|
+
@health.detach
|
|
153
|
+
@native&.stop_server
|
|
154
|
+
stop_log_pump(log_pump)
|
|
155
|
+
@native = nil
|
|
156
|
+
Tonic::Rails._set_shutting_down(false)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# True in a worker process of a multi-process server, whose parent
|
|
160
|
+
# logs startup and shutdown for all of them.
|
|
161
|
+
def worker?
|
|
162
|
+
!@worker_index.nil?
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def worker_label
|
|
166
|
+
worker? ? "worker #{@worker_index}: " : ""
|
|
167
|
+
end
|
|
168
|
+
|
|
128
169
|
def build_native_server
|
|
129
170
|
config = @configuration
|
|
130
171
|
native = TonicRails::Native::Server.new(config.bind_address, config.max_message_size)
|
|
@@ -267,13 +308,15 @@ module Tonic
|
|
|
267
308
|
|
|
268
309
|
def shutdown(workers)
|
|
269
310
|
logger = @configuration.logger
|
|
311
|
+
# In a worker, the parent logs shutdown once for all of them.
|
|
312
|
+
lifecycle = worker? ? nil : logger
|
|
270
313
|
@state = :stopping
|
|
271
314
|
Tonic::Rails._set_shutting_down(true)
|
|
272
|
-
|
|
315
|
+
lifecycle&.info("tonic-rails: shutting down, draining for up to #{@configuration.drain_timeout}s")
|
|
273
316
|
|
|
274
317
|
@native.begin_shutdown
|
|
275
318
|
unless @native.wait_for_drain(@configuration.drain_timeout.to_f)
|
|
276
|
-
logger&.warn("tonic-rails: drain timeout expired; closing remaining connections")
|
|
319
|
+
logger&.warn("tonic-rails: #{worker_label}drain timeout expired; closing remaining connections")
|
|
277
320
|
end
|
|
278
321
|
@native.stop_workers
|
|
279
322
|
workers.each do |worker|
|
|
@@ -281,7 +324,7 @@ module Tonic
|
|
|
281
324
|
rescue Exception # rubocop:disable Lint/RescueException
|
|
282
325
|
# Already logged by the worker.
|
|
283
326
|
end
|
|
284
|
-
|
|
327
|
+
lifecycle&.info("tonic-rails: stopped")
|
|
285
328
|
end
|
|
286
329
|
|
|
287
330
|
def log_grpc_web(logger, config)
|
|
@@ -306,10 +349,9 @@ module Tonic
|
|
|
306
349
|
end
|
|
307
350
|
logger.info("tonic-rails: reflection enabled") if config.reflection
|
|
308
351
|
log_grpc_web(logger, config)
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
)
|
|
352
|
+
threads = "#{config.dispatch_threads} dispatch threads"
|
|
353
|
+
threads = "#{config.processes} processes × #{threads} each" if config.multi_process?
|
|
354
|
+
logger.info("tonic-rails: serving #{@router.service_names.join(', ')} on #{config.bind_address} (#{threads})")
|
|
313
355
|
end
|
|
314
356
|
end
|
|
315
357
|
end
|
data/lib/tonic/rails/version.rb
CHANGED
data/lib/tonic-rails.rb
CHANGED
|
@@ -19,7 +19,9 @@ require "tonic/rails/instrumentation"
|
|
|
19
19
|
require "tonic/rails/base"
|
|
20
20
|
require "tonic/rails/health"
|
|
21
21
|
require "tonic/rails/reflection"
|
|
22
|
+
require "tonic/rails/cluster"
|
|
22
23
|
require "tonic/rails/server"
|
|
24
|
+
require "tonic/rails/client"
|
|
23
25
|
|
|
24
26
|
module Tonic
|
|
25
27
|
module Rails
|
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.6.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Matt Tanous
|
|
@@ -73,6 +73,7 @@ files:
|
|
|
73
73
|
- ext/tonic_rails_native/Cargo.toml
|
|
74
74
|
- ext/tonic_rails_native/extconf.rb
|
|
75
75
|
- ext/tonic_rails_native/src/body.rs
|
|
76
|
+
- ext/tonic_rails_native/src/client.rs
|
|
76
77
|
- ext/tonic_rails_native/src/dispatch.rs
|
|
77
78
|
- ext/tonic_rails_native/src/framing.rs
|
|
78
79
|
- ext/tonic_rails_native/src/gvl.rs
|
|
@@ -90,6 +91,8 @@ files:
|
|
|
90
91
|
- lib/puma/plugin/tonic_rails.rb
|
|
91
92
|
- lib/tonic-rails.rb
|
|
92
93
|
- lib/tonic/rails/base.rb
|
|
94
|
+
- lib/tonic/rails/client.rb
|
|
95
|
+
- lib/tonic/rails/cluster.rb
|
|
93
96
|
- lib/tonic/rails/configuration.rb
|
|
94
97
|
- lib/tonic/rails/health.rb
|
|
95
98
|
- lib/tonic/rails/instrumentation.rb
|