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.
@@ -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?(EVENT)
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(EVENT, payload) do
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
@@ -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
@@ -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
- def start(install_signal_handlers: true)
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
- @native = build_native_server
60
- @native.start_server
61
- log_pump = spawn_log_pump
62
- @health.attach(@native)
63
- @state = :running
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
- logger&.info("tonic-rails: shutting down, draining for up to #{@configuration.drain_timeout}s")
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
- logger&.info("tonic-rails: stopped")
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
- logger.info(
310
- "tonic-rails: serving #{@router.service_names.join(', ')} on #{config.bind_address} " \
311
- "(#{config.dispatch_threads} dispatch threads)",
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
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Tonic
4
4
  module Rails
5
- VERSION = "0.4.0"
5
+ VERSION = "0.6.0"
6
6
  end
7
7
  end
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.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