tonic-rails 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -20,7 +20,7 @@ use magnus::{
20
20
  Error, RHash, RString, Ruby, Value, prelude::*, r_hash::ForEach, typed_data::DataTypeFunctions,
21
21
  value::Opaque,
22
22
  };
23
- use parking_lot::Mutex;
23
+ use parking_lot::{Condvar, Mutex};
24
24
  use rb_sys::{RB_NOGVL_INTR_FAIL, rb_nogvl, rb_thread_call_with_gvl};
25
25
  use tokio::sync::{Notify, mpsc};
26
26
 
@@ -100,25 +100,40 @@ where
100
100
 
101
101
  /// Minimal single-future executor that parks the current (non-tokio)
102
102
  /// thread. tokio's channel and `Notify` futures are runtime-agnostic.
103
+ ///
104
+ /// It parks on a pthread condition variable rather than
105
+ /// `std::thread::park`, which on macOS uses libdispatch: libdispatch
106
+ /// traps in the child of a multi-threaded process, and the client blocks
107
+ /// here in processes forked after it made calls.
103
108
  pub fn block_on<F: Future>(fut: F) -> F::Output {
104
- struct ThreadWaker(std::thread::Thread);
105
- impl Wake for ThreadWaker {
109
+ #[derive(Default)]
110
+ struct Signal {
111
+ woken: Mutex<bool>,
112
+ condvar: Condvar,
113
+ }
114
+ impl Wake for Signal {
106
115
  fn wake(self: Arc<Self>) {
107
- self.0.unpark();
116
+ self.wake_by_ref();
108
117
  }
109
118
  fn wake_by_ref(self: &Arc<Self>) {
110
- self.0.unpark();
119
+ *self.woken.lock() = true;
120
+ self.condvar.notify_one();
111
121
  }
112
122
  }
113
123
 
114
- let waker = Waker::from(Arc::new(ThreadWaker(std::thread::current())));
124
+ let signal = Arc::new(Signal::default());
125
+ let waker = Waker::from(signal.clone());
115
126
  let mut cx = Context::from_waker(&waker);
116
127
  let mut fut = std::pin::pin!(fut);
117
128
  loop {
118
129
  if let Poll::Ready(value) = fut.as_mut().poll(&mut cx) {
119
130
  return value;
120
131
  }
121
- std::thread::park();
132
+ let mut woken = signal.woken.lock();
133
+ while !*woken {
134
+ signal.condvar.wait(&mut woken);
135
+ }
136
+ *woken = false;
122
137
  }
123
138
  }
124
139
 
@@ -20,6 +20,7 @@
20
20
  //! Modules:
21
21
  //! - [`framing`] — gRPC wire format (frames, gzip, timeouts, trailers)
22
22
  //! - [`body`] — response bodies and request stats
23
+ //! - [`client`] — `TonicRails::Native::Channel` and `ResponseStream`, for outgoing calls
23
24
  //! - [`dispatch`] — the fallback tower service that queues RPCs for Ruby
24
25
  //! - [`gvl`] — GVL release/acquire and the Ruby stream objects
25
26
  //! - [`log`] — transport log events for Ruby's logger
@@ -27,6 +28,7 @@
27
28
  //! - [`server`] — `TonicRails::Native::Server`
28
29
 
29
30
  mod body;
31
+ mod client;
30
32
  mod dispatch;
31
33
  mod framing;
32
34
  mod gvl;
@@ -36,6 +38,7 @@ mod server;
36
38
 
37
39
  use magnus::{Error, Ruby, function, method, prelude::*};
38
40
 
41
+ use client::{NativeChannel, ResponseStream};
39
42
  use gvl::{RequestReader, StreamSender};
40
43
  use server::NativeServer;
41
44
 
@@ -95,5 +98,16 @@ fn init(ruby: &Ruby) -> Result<(), Error> {
95
98
  reader.define_method("next", method!(RequestReader::next, 0))?;
96
99
  reader.define_method("error", method!(RequestReader::error, 0))?;
97
100
 
101
+ let channel = native.define_class("Channel", ruby.class_object())?;
102
+ channel.define_singleton_method("new", function!(NativeChannel::new, 4))?;
103
+ channel.define_method("unary", method!(NativeChannel::unary, 4))?;
104
+ channel.define_method("server_streaming", method!(NativeChannel::server_streaming, 4))?;
105
+
106
+ let stream = native.define_class("ResponseStream", ruby.class_object())?;
107
+ stream.undef_default_alloc_func();
108
+ stream.define_method("next", method!(ResponseStream::next, 0))?;
109
+ stream.define_method("result", method!(ResponseStream::result, 0))?;
110
+ stream.define_method("close", method!(ResponseStream::close, 0))?;
111
+
98
112
  Ok(())
99
113
  }
@@ -248,7 +248,10 @@ module Tonic
248
248
 
249
249
  @request = build_request(reader, spec)
250
250
  response = nil
251
- run_callbacks(:rpc) { response = respond(public_send(spec[:handler], @request), spec, sender) }
251
+ # Calls this handler makes with Tonic::Rails::Client default to its deadline.
252
+ Client.with_deadline(@grpc_deadline_at) do
253
+ run_callbacks(:rpc) { response = respond(public_send(spec[:handler], @request), spec, sender) }
254
+ end
252
255
  response
253
256
  rescue RpcError
254
257
  raise
@@ -0,0 +1,371 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/inflector"
4
+
5
+ module Tonic
6
+ module Rails
7
+ # Calls other gRPC services, without the grpc gem.
8
+ #
9
+ # ITEMS = Tonic::Rails::Client.new("items.internal:50051", deadline: 2)
10
+ #
11
+ # items = ITEMS.stub("demo.v1.ItemService")
12
+ # item = items.get_item(Demo::V1::GetItemRequest.new(id: 1))
13
+ # item = items.get_item(id: 1)
14
+ # item = items.get_item({ id: 1 }, deadline: 0.5, metadata: { "x-request-id" => request_id })
15
+ # items.list_items(category: "books").each { |item| ... }
16
+ #
17
+ # Stubs are built from the protobuf descriptors that
18
+ # `tonic_rails:compile_protos` (protoc --ruby_out) generates; no
19
+ # `_services_pb.rb` is needed. A failed call raises
20
+ # Tonic::Rails::RpcError.
21
+ #
22
+ # A client holds one HTTP/2 connection and is safe to share between
23
+ # threads, so create it once (a constant or an initializer). It keeps
24
+ # working across `fork`: each process connects on its own, with no
25
+ # hooks to call.
26
+ class Client
27
+ # A response with its headers and trailers; see `return_call:`.
28
+ Call = Data.define(:response, :headers, :trailers)
29
+
30
+ DEFAULT_MAX_MESSAGE_SIZE = 4 * 1024 * 1024
31
+
32
+ # @return [String] "host:port"
33
+ attr_reader :address
34
+
35
+ # @return [Numeric, nil] default deadline in seconds
36
+ attr_reader :deadline
37
+
38
+ # @param address [String] "host:port"
39
+ # @param tls [Boolean] false for plaintext, which must be explicit
40
+ # @param tls_ca_path [String, nil] CA certificates (PEM) to trust instead
41
+ # of the system's root certificates
42
+ # @param tls_cert_path [String, nil] client certificate (PEM), for mutual TLS
43
+ # @param tls_key_path [String, nil] client private key (PEM), for mutual TLS
44
+ # @param tls_server_name [String, nil] name to verify the server's
45
+ # certificate against, if not the host in `address`
46
+ # @param deadline [Numeric, nil] seconds each call may take, unless the
47
+ # call sets its own
48
+ # @param metadata [Hash] sent with every call (see Tonic::Rails::Metadata)
49
+ # @param max_message_size [Integer] largest message sent or received, in bytes
50
+ # @param connect_timeout [Numeric] seconds to wait for a connection
51
+ def initialize(address, tls: true, tls_ca_path: nil, tls_cert_path: nil, tls_key_path: nil,
52
+ tls_server_name: nil, deadline: nil, metadata: {}, max_message_size: DEFAULT_MAX_MESSAGE_SIZE,
53
+ connect_timeout: 5)
54
+ Tonic::Rails.native_extension!
55
+ validate_address!(address)
56
+ validate_tls!(tls, tls_cert_path, tls_key_path)
57
+ positive!(:deadline, deadline) unless deadline.nil?
58
+ positive!(:connect_timeout, connect_timeout)
59
+ unless max_message_size.is_a?(Integer) && max_message_size.positive?
60
+ raise ArgumentError, "max_message_size must be a positive Integer"
61
+ end
62
+
63
+ @address = address
64
+ @deadline = deadline
65
+ @metadata = Metadata.encode(metadata)
66
+ @stubs = {}
67
+ tls_options = [read_pem(tls_ca_path, "CA"), read_pem(tls_cert_path, "certificate"),
68
+ read_pem(tls_key_path, "key"), tls_server_name]
69
+ @native = TonicRails::Native::Channel.new(
70
+ "#{tls ? 'https' : 'http'}://#{address}", max_message_size, (connect_timeout * 1000).ceil,
71
+ (tls_options if tls)
72
+ )
73
+ end
74
+
75
+ # A stub with one method per RPC of the service, named in snake_case
76
+ # (`GetItem` becomes `get_item`).
77
+ #
78
+ # @param service [String, Google::Protobuf::ServiceDescriptor, #service_name]
79
+ # the fully-qualified service name, its descriptor, or a grpc gem
80
+ # service class (`Demo::V1::ItemService::Service`)
81
+ # @return [Tonic::Rails::Client::Stub]
82
+ def stub(service)
83
+ descriptor = service_descriptor(service)
84
+ @stubs[descriptor.name] ||= Stub.for(descriptor).new(self)
85
+ end
86
+
87
+ def inspect
88
+ "#<#{self.class.name} #{address}>"
89
+ end
90
+
91
+ # The deadline of the RPC this thread is handling, as a monotonic
92
+ # clock time; outgoing calls default to it.
93
+ # @api private
94
+ def self.current_deadline
95
+ Thread.current[:tonic_rails_deadline]
96
+ end
97
+
98
+ # @api private
99
+ def self.with_deadline(deadline)
100
+ previous = Thread.current[:tonic_rails_deadline]
101
+ Thread.current[:tonic_rails_deadline] = deadline
102
+ yield
103
+ ensure
104
+ Thread.current[:tonic_rails_deadline] = previous
105
+ end
106
+
107
+ # Make a unary call. Stubs call this; use them instead.
108
+ # @api private
109
+ def unary(path, input, output, request, deadline: nil, metadata: {}, return_call: false)
110
+ message = request_message(path, input, request)
111
+ metadata = call_metadata(metadata)
112
+ timeout_ms = remaining_ms(deadline_at(deadline))
113
+
114
+ Instrumentation.instrument_request(path, address, metadata) do
115
+ code, error_message, response, headers, trailers, details =
116
+ @native.unary(path, input.encode(message), metadata, timeout_ms)
117
+ headers = decode_metadata(headers)
118
+ trailers = decode_metadata(trailers)
119
+ check_status!(code, error_message, trailers, details)
120
+
121
+ response = decode_response(output, response, path)
122
+ return_call ? Call.new(response: response, headers: headers, trailers: trailers) : response
123
+ end
124
+ end
125
+
126
+ # Start a server-streaming call. Stubs call this; use them instead.
127
+ # @api private
128
+ def server_streaming(path, input, output, request, deadline: nil, metadata: {})
129
+ message = request_message(path, input, request)
130
+ metadata = call_metadata(metadata)
131
+ # Fixed now, so the stream keeps the deadline of the RPC this
132
+ # thread is handling even if it's read later.
133
+ deadline_at = deadline_at(deadline)
134
+
135
+ ServerStream.new(path) do |stream, block|
136
+ timeout_ms = remaining_ms(deadline_at)
137
+ Instrumentation.instrument_request(path, address, metadata) do
138
+ native = @native.server_streaming(path, input.encode(message), metadata, timeout_ms)
139
+ begin
140
+ while (bytes = native.next)
141
+ block.call(decode_response(output, bytes, path))
142
+ end
143
+ code, error_message, _, headers, trailers, details = native.result
144
+ trailers = decode_metadata(trailers)
145
+ stream.finish(decode_metadata(headers), trailers)
146
+ check_status!(code, error_message, trailers, details)
147
+ ensure
148
+ native.close
149
+ end
150
+ end
151
+ end
152
+ end
153
+
154
+ private
155
+
156
+ def validate_address!(address)
157
+ return if address.is_a?(String) && address.match?(%r{\A[^\s/]+:\d+\z})
158
+
159
+ raise ArgumentError, "address must be \"host:port\", got #{address.inspect}"
160
+ end
161
+
162
+ def validate_tls!(tls, cert_path, key_path)
163
+ raise ArgumentError, "tls must be true or false" unless [true, false].include?(tls)
164
+ raise ArgumentError, "tls_cert_path and tls_key_path must be set together" if cert_path.nil? != key_path.nil?
165
+ raise ArgumentError, "tls_cert_path and tls_key_path need tls: true" if cert_path && !tls
166
+ end
167
+
168
+ def positive!(name, value)
169
+ raise ArgumentError, "#{name} must be a positive number of seconds" unless value.is_a?(Numeric) && value.positive?
170
+ end
171
+
172
+ def read_pem(path, what)
173
+ return unless path
174
+ raise ArgumentError, "TLS #{what} file not found: #{path}" unless File.file?(path)
175
+
176
+ File.read(path)
177
+ end
178
+
179
+ def service_descriptor(service)
180
+ return service if service.is_a?(Google::Protobuf::ServiceDescriptor)
181
+
182
+ name = service.respond_to?(:service_name) ? service.service_name : service.to_s
183
+ descriptor = Google::Protobuf::DescriptorPool.generated_pool.lookup(name)
184
+ return descriptor if descriptor.is_a?(Google::Protobuf::ServiceDescriptor)
185
+
186
+ raise NotConfiguredError,
187
+ "#{name} is not a known protobuf service. Require its generated _pb.rb file (protoc --ruby_out) first."
188
+ end
189
+
190
+ def request_message(path, input, request)
191
+ case request
192
+ when input then request
193
+ when Hash then input.new(request)
194
+ else raise TypeError, "#{path} expects a #{input} or Hash, got #{request.class}"
195
+ end
196
+ end
197
+
198
+ def call_metadata(metadata)
199
+ metadata.empty? ? @metadata : @metadata.merge(Metadata.encode(metadata))
200
+ end
201
+
202
+ # When the call must end, as a monotonic clock time: the call's own
203
+ # deadline, or else the earlier of the client's default and the
204
+ # deadline of the RPC being handled.
205
+ def deadline_at(deadline)
206
+ now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
207
+ case deadline
208
+ when nil then [(now + @deadline if @deadline), self.class.current_deadline].compact.min
209
+ when Numeric then now + deadline
210
+ when Time then now + (deadline - Time.now)
211
+ else raise ArgumentError, "deadline must be seconds (Numeric) or a Time, got #{deadline.class}"
212
+ end
213
+ end
214
+
215
+ # Milliseconds left until `deadline_at`, or nil for no deadline.
216
+ def remaining_ms(deadline_at)
217
+ return unless deadline_at
218
+
219
+ remaining = deadline_at - Process.clock_gettime(Process::CLOCK_MONOTONIC)
220
+ raise RpcError.new("deadline exceeded", code: :deadline_exceeded) unless remaining.positive?
221
+
222
+ (remaining * 1000).ceil
223
+ end
224
+
225
+ def check_status!(code, message, trailers, details)
226
+ return if code.zero?
227
+
228
+ raise RpcError.new(message.empty? ? nil : message, code: code, metadata: trailers,
229
+ details: RpcError.decode_details(details))
230
+ end
231
+
232
+ # Wire-form [name, value] pairs to a Hash: `-bin` values decoded,
233
+ # repeated names collected into an Array, and transport headers
234
+ # (content-type, grpc-*) dropped.
235
+ def decode_metadata(pairs)
236
+ pairs.each_with_object({}) do |(name, value), metadata|
237
+ next if name.start_with?("grpc-") || Metadata::RESERVED.include?(name)
238
+
239
+ value = value.unpack1("m") if name.end_with?("-bin")
240
+ metadata[name] = metadata.key?(name) ? [*metadata[name], value] : value
241
+ end
242
+ end
243
+
244
+ def decode_response(output, bytes, path)
245
+ output.decode(bytes)
246
+ rescue Google::Protobuf::ParseError
247
+ raise RpcError.new("invalid protobuf response from #{path}", code: :internal)
248
+ end
249
+
250
+ # Calls an RPC's methods. Built per service from its descriptor; get
251
+ # one with Client#stub.
252
+ class Stub
253
+ class << self
254
+ # @api private
255
+ attr_reader :service_name
256
+
257
+ # @api private
258
+ def for(descriptor)
259
+ Class.new(self) do
260
+ @service_name = descriptor.name
261
+ descriptor.each { |method| define_rpc(method) }
262
+ end
263
+ end
264
+
265
+ def inspect
266
+ service_name ? "#{Stub.name}(#{service_name})" : super
267
+ end
268
+
269
+ private
270
+
271
+ def define_rpc(method)
272
+ path = "/#{service_name}/#{method.name}"
273
+ name = ActiveSupport::Inflector.underscore(method.name)
274
+ if method.client_streaming
275
+ define_method(name) do |*, **|
276
+ raise NotImplementedError,
277
+ "#{path} is a client-streaming RPC; the tonic-rails client makes unary and server-streaming calls"
278
+ end
279
+ return
280
+ end
281
+
282
+ input = method.input_type.msgclass
283
+ output = method.output_type.msgclass
284
+ if method.server_streaming
285
+ define_method(name) do |request = nil, deadline: nil, metadata: {}, **fields|
286
+ request = rpc_request(name, request, fields)
287
+ @client.server_streaming(path, input, output, request, deadline: deadline, metadata: metadata)
288
+ end
289
+ else
290
+ # `get_item(id: 1)` works as well as `get_item({ id: 1 })`.
291
+ define_method(name) do |request = nil, deadline: nil, metadata: {}, return_call: false, **fields|
292
+ request = rpc_request(name, request, fields)
293
+ @client.unary(path, input, output, request, deadline: deadline, metadata: metadata,
294
+ return_call: return_call)
295
+ end
296
+ end
297
+ end
298
+ end
299
+
300
+ # @return [Tonic::Rails::Client]
301
+ attr_reader :client
302
+
303
+ def initialize(client)
304
+ @client = client
305
+ end
306
+
307
+ def inspect
308
+ "#<#{self.class.inspect} #{client.address}>"
309
+ end
310
+
311
+ private
312
+
313
+ def rpc_request(name, request, fields)
314
+ unless request.nil? || fields.empty?
315
+ raise ArgumentError, "#{name}: pass the request or its fields, not both (#{fields.keys.join(', ')})"
316
+ end
317
+
318
+ request || fields
319
+ end
320
+ end
321
+
322
+ # The responses of a server-streaming call. Nothing is sent until
323
+ # it's read; then messages are yielded as they arrive, a few at most
324
+ # buffered ahead of the reader.
325
+ #
326
+ # items.list_items(category: "books").each { |item| ... }
327
+ # items.list_items(category: "books").first(10)
328
+ #
329
+ # Stopping early (`break`, `first(n)`, an exception) cancels the call.
330
+ # An error from the server raises Tonic::Rails::RpcError after the
331
+ # messages that came before it. A stream can be read once.
332
+ class ServerStream
333
+ include Enumerable
334
+
335
+ # @return [Hash, nil] response headers, once the stream has ended
336
+ attr_reader :headers
337
+
338
+ # @return [Hash, nil] trailing metadata, once the stream has ended
339
+ attr_reader :trailers
340
+
341
+ # @api private
342
+ def initialize(path, &read)
343
+ @path = path
344
+ @read = read
345
+ end
346
+
347
+ # @yieldparam response [Google::Protobuf::AbstractMessage]
348
+ # @return [self, Enumerator] an Enumerator without a block
349
+ def each(&block)
350
+ return enum_for(:each) unless block
351
+ raise Error, "the #{@path} stream has already been read" unless @read
352
+
353
+ read = @read
354
+ @read = nil
355
+ read.call(self, block)
356
+ self
357
+ end
358
+
359
+ # @api private
360
+ def finish(headers, trailers)
361
+ @headers = headers
362
+ @trailers = trailers
363
+ end
364
+
365
+ def inspect
366
+ "#<#{self.class.name} #{@path}>"
367
+ end
368
+ end
369
+ end
370
+ end
371
+ end
@@ -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
@@ -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
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Tonic
4
4
  module Rails
5
- VERSION = "0.5.0"
5
+ VERSION = "0.6.0"
6
6
  end
7
7
  end
data/lib/tonic-rails.rb CHANGED
@@ -21,6 +21,7 @@ require "tonic/rails/health"
21
21
  require "tonic/rails/reflection"
22
22
  require "tonic/rails/cluster"
23
23
  require "tonic/rails/server"
24
+ require "tonic/rails/client"
24
25
 
25
26
  module Tonic
26
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.5.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,7 @@ 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
93
95
  - lib/tonic/rails/cluster.rb
94
96
  - lib/tonic/rails/configuration.rb
95
97
  - lib/tonic/rails/health.rb