gruf 2.14.0 → 2.15.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: aebffd1712a25d5025e60e61797fd6089417db3d79c5be2de9374b3eeda71571
4
- data.tar.gz: 82324e0b421ac2d82d2133dcf482746e6c6faaa31f7cbf50de3adaf31ae857ac
3
+ metadata.gz: 5186fbfc554b119075aff88683ee7f1661a884d96d9766e59664d10805bd4ab2
4
+ data.tar.gz: 68b995a644b15dc703595df4e35a25539e0edf77d5f5218bffa28253b60eedbf
5
5
  SHA512:
6
- metadata.gz: eae3c9439c9b8e9c6b7d26a30c7485aac0c3919bb174a9b2a8b23b7f863f5f2ddacbb52c87489724ac9e4345214ec585f257f852e94d8af27486ec12bb971aaf
7
- data.tar.gz: 0fb952bb1e354fc5f756e1263b5a1929e68647df757d5da97d0689aa4f09185abd2cd4f91d023faf56fa75fc9b828098c670cf17a5bd171b2511ae0ae5d4096b
6
+ metadata.gz: 8328cccfcc712c21cb5a477939762524d774d3d45a0882217fe3b7d701cfda90e9dcd27af7ab4c4cbc73ca46d420d85e55be3712efc950590212658030217c40
7
+ data.tar.gz: 2cf04dce0c4e378edf56ab6b204293ce2b4dc660d2ac103ac55d99e893891ac3a76feb2e05dee4ac3371b47083fd8523441e7e2c87881bacd2511b016a1965b3
data/CHANGELOG.md CHANGED
@@ -2,6 +2,24 @@ Changelog for the gruf gem. This includes internal history before the gem was ma
2
2
 
3
3
  ### Pending release
4
4
 
5
+ ### 2.15.1
6
+
7
+ - Fix issue where GRPC_SERVER_POOL_KEEP_ALIVE and GRPC_SERVER_POLL_PERIOD when set via ENV are not cast to int
8
+
9
+ ### 2.15.0
10
+
11
+ - NOTE: This changes the way that gruf controllers are autoloaded. See [UPGRADING.md] for more details.
12
+ - Autoload Gruf Controllers with zeitwerk, allowing for code reloading in development environments
13
+ - Add `GRUF_CONTROLLERS_PATH` ENV to allow ENV-based runtime configuration of path to gruf controller files
14
+ - Move `ServiceBinder` from instance to static class for performance improvements
15
+ - Use zeitwerk for autoloading
16
+ - Update gem metadata
17
+ - Update `Gruf:Configuration#environment` to use `ENV.fetch`
18
+
19
+ ### 2.14.1
20
+
21
+ - Fix issue where the server object hits thread contention in certain race conditions
22
+
5
23
  ### 2.14.0
6
24
 
7
25
  - Set default client host to 0.0.0.0:9001 (same as default server host)
@@ -9,9 +27,9 @@ Changelog for the gruf gem. This includes internal history before the gem was ma
9
27
 
10
28
  ### 2.13.1
11
29
 
12
- - Fix issue with race condition in server starts where servers may fail to bind connections and never reach
30
+ - Fix issue with race condition in server starts where servers may fail to bind connections and never reach
13
31
  serving state (fixes #147)
14
-
32
+
15
33
  ### 2.13.0
16
34
 
17
35
  - Remove server mutex handling in deference to core grpc signal handling
@@ -23,7 +41,7 @@ Changelog for the gruf gem. This includes internal history before the gem was ma
23
41
 
24
42
  ### 2.11.0
25
43
 
26
- - Restrict grpc gem to <= 1.41.0 due to regressions in grpc 1.42.x
44
+ - Restrict grpc gem to <= 1.41.0 due to regressions in grpc 1.42.x
27
45
  - Fallback to stdout logger at INFO if no logger is setup
28
46
  - Better handling of namespace collisions with Rails
29
47
  - Add `GRPC_SERVER_HOST` and `GRPC_SERVER_PORT` for ENV configuration of the server host+port
@@ -63,7 +81,7 @@ Changelog for the gruf gem. This includes internal history before the gem was ma
63
81
 
64
82
  ### 2.8.0
65
83
 
66
- - Pass the controller request object into the request logging formatters [#92]
84
+ - Pass the controller request object into the request logging formatters [#92]
67
85
 
68
86
  ### 2.7.1
69
87
 
@@ -76,12 +94,12 @@ Changelog for the gruf gem. This includes internal history before the gem was ma
76
94
  ### 2.6.1
77
95
 
78
96
  - Add frozen_string_literal: true to files, update rubocop to 0.68
79
-
97
+
80
98
  ### 2.6.0
81
99
 
82
100
  - Drop Ruby 2.2 support
83
- - Abstract gruf controller's send to make it usable in filters
84
- - Adjusts configuration reset into a Railtie for Rails systems to ensure proper OOE
101
+ - Abstract gruf controller's send to make it usable in filters
102
+ - Adjusts configuration reset into a Railtie for Rails systems to ensure proper OOE
85
103
  - Bump rubocop to 0.64, address violations, update activesupport/concurrent-ruby dependencies to have a min version
86
104
 
87
105
  ### 2.5.2
data/gruf.gemspec CHANGED
@@ -15,8 +15,7 @@
15
15
  # COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
16
16
  # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
17
17
  #
18
- $LOAD_PATH.push File.expand_path('lib', __dir__)
19
- require 'gruf/version'
18
+ require_relative 'lib/gruf/version'
20
19
 
21
20
  Gem::Specification.new do |spec|
22
21
  spec.name = 'gruf'
@@ -35,7 +34,14 @@ Gem::Specification.new do |spec|
35
34
 
36
35
  spec.required_ruby_version = '>= 2.6', '< 3.2'
37
36
 
38
- spec.metadata['rubygems_mfa_required'] = 'true'
37
+ spec.metadata = {
38
+ 'bug_tracker_uri' => 'https://github.com/bigcommerce/gruf/issues',
39
+ 'changelog_uri' => 'https://github.com/bigcommerce/gruf/CHANGELOG.md',
40
+ 'homepage_uri' => 'https://github.com/bigcommerce/gruf',
41
+ 'rubygems_mfa_required' => 'true',
42
+ 'source_code_uri' => 'https://github.com/bigcommerce/gruf',
43
+ 'wiki_uri' => 'https://github.com/bigcommerce/gruf/wiki'
44
+ }
39
45
 
40
46
  spec.add_development_dependency 'bundler-audit', '>= 0.6'
41
47
  # rubocop:disable Gemspec/RubyVersionGlobalsUsage
@@ -64,4 +70,5 @@ Gem::Specification.new do |spec|
64
70
  spec.add_runtime_dependency 'json', '>= 2.3'
65
71
  spec.add_runtime_dependency 'slop', '>= 4.6'
66
72
  spec.add_runtime_dependency 'thwait', '>= 0.1'
73
+ spec.add_runtime_dependency 'zeitwerk', '>= 2'
67
74
  end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright (c) 2022-present, BigCommerce Pty. Ltd. All rights reserved
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
6
+ # documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
7
+ # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit
8
+ # persons to whom the Software is furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
11
+ # Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
14
+ # WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
15
+ # COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
16
+ # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
17
+ #
18
+ module Gruf
19
+ ##
20
+ # Module for accessing Gruf zeitwerk-based autoloaders
21
+ #
22
+ module Autoloaders
23
+ class << self
24
+ include Enumerable
25
+
26
+ ##
27
+ # Initialize the autoloaders with a given controllers path
28
+ #
29
+ # @param [String] controllers_path The path to Gruf Controllers
30
+ #
31
+ def load!(controllers_path:)
32
+ controllers(controllers_path: controllers_path)
33
+ end
34
+
35
+ ##
36
+ # Enumerate across the managed set of autoloaders
37
+ #
38
+ def each
39
+ yield controllers
40
+ end
41
+
42
+ ##
43
+ # Reload all files managed by the autoloader
44
+ #
45
+ def reload
46
+ each(&:reload)
47
+ end
48
+
49
+ ##
50
+ # Lazily instantiate and memoize the Gruf Controllers autoloader in a thread-safe manner
51
+ #
52
+ # @return [::Gruf::Controllers::Autoloader]
53
+ #
54
+ # rubocop:disable ThreadSafety/InstanceVariableInClassMethod
55
+ def controllers(controllers_path: nil)
56
+ controllers_mutex do
57
+ @controllers ||= ::Gruf::Controllers::Autoloader.new(path: controllers_path || ::Gruf.controllers_path)
58
+ end
59
+ end
60
+ # rubocop:enable ThreadSafety/InstanceVariableInClassMethod
61
+
62
+ ##
63
+ # Handle mutations to the controllers autoloader in a thread-safe manner
64
+ #
65
+ # rubocop:disable ThreadSafety/InstanceVariableInClassMethod
66
+ def controllers_mutex(&block)
67
+ @controllers_mutex ||= begin
68
+ require 'monitor'
69
+ Monitor.new
70
+ end
71
+ @controllers_mutex.synchronize(&block)
72
+ end
73
+ # rubocop:enable ThreadSafety/InstanceVariableInClassMethod
74
+ end
75
+ end
76
+ end
@@ -23,8 +23,14 @@ module Gruf
23
23
  # Handles execution of the gruf binstub, along with command-line arguments
24
24
  #
25
25
  class Executor
26
+ class NoServicesBoundError < StandardError; end
27
+
26
28
  ##
27
29
  # @param [Hash|ARGV]
30
+ # @param [::Gruf::Server|NilClass] server
31
+ # @param [Array<Class>|NilClass] services
32
+ # @param [Gruf::Hooks::Executor|NilClass] hook_executor
33
+ # @param [Logger|NilClass] logger
28
34
  #
29
35
  def initialize(
30
36
  args = ARGV,
@@ -35,7 +41,7 @@ module Gruf
35
41
  )
36
42
  @args = args
37
43
  setup! # ensure we set some defaults from CLI here so we can allow configuration
38
- @services = services || Gruf.services
44
+ @services = services.is_a?(Array) ? services : []
39
45
  @hook_executor = hook_executor || Gruf::Hooks::Executor.new(hooks: Gruf.hooks&.prepare)
40
46
  @server = server || Gruf::Server.new(Gruf.server_options)
41
47
  @logger = logger || Gruf.logger || ::Logger.new($stderr)
@@ -46,6 +52,17 @@ module Gruf
46
52
  #
47
53
  def run
48
54
  exception = nil
55
+ # wait to load controllers until last possible second to allow late configuration
56
+ ::Gruf.autoloaders.load!(controllers_path: Gruf.controllers_path)
57
+ # allow lazy registering globally as late as possible, this allows more flexible binstub injections
58
+ @services = ::Gruf.services unless @services&.any?
59
+
60
+ unless @services.any?
61
+ raise NoServicesBoundError,
62
+ 'No services bound to this gruf process; please bind a service to a Gruf controller ' \
63
+ 'to start the server successfully'
64
+ end
65
+
49
66
  begin
50
67
  @services.each { |s| @server.add_service(s) }
51
68
  @hook_executor.call(:before_server_start, server: @server)
@@ -37,33 +37,5 @@ module Gruf
37
37
  super
38
38
  end
39
39
  end
40
-
41
- ##
42
- # See https://github.com/grpc/grpc-go/blob/master/codes/codes.go for a detailed summary of each error type
43
- #
44
- module Errors
45
- class Base < Gruf::Client::Error; end
46
- class Error < Base; end
47
- class Validation < Base; end
48
-
49
- class Ok < Base; end
50
-
51
- class InvalidArgument < Validation; end
52
- class NotFound < Validation; end
53
- class AlreadyExists < Validation; end
54
- class OutOfRange < Validation; end
55
-
56
- class Cancelled < Error; end
57
- class DataLoss < Error; end
58
- class DeadlineExceeded < Error; end
59
- class FailedPrecondition < Error; end
60
- class Internal < Error; end
61
- class PermissionDenied < Error; end
62
- class ResourceExhausted < Error; end
63
- class Unauthenticated < Error; end
64
- class Unavailable < Error; end
65
- class Unimplemented < Error; end
66
- class Unknown < Error; end
67
- end
68
40
  end
69
41
  end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright (c) 2017-present, BigCommerce Pty. Ltd. All rights reserved
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
6
+ # documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
7
+ # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit
8
+ # persons to whom the Software is furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
11
+ # Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
14
+ # WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
15
+ # COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
16
+ # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
17
+ #
18
+ module Gruf
19
+ class Client < SimpleDelegator
20
+ ##
21
+ # See https://github.com/grpc/grpc-go/blob/master/codes/codes.go for a detailed summary of each error type
22
+ #
23
+ module Errors
24
+ class Base < Gruf::Client::Error; end
25
+ class Error < Base; end
26
+ class Validation < Base; end
27
+
28
+ class Ok < Base; end
29
+
30
+ class InvalidArgument < Validation; end
31
+ class NotFound < Validation; end
32
+ class AlreadyExists < Validation; end
33
+ class OutOfRange < Validation; end
34
+
35
+ class Cancelled < Error; end
36
+ class DataLoss < Error; end
37
+ class DeadlineExceeded < Error; end
38
+ class FailedPrecondition < Error; end
39
+ class Internal < Error; end
40
+ class PermissionDenied < Error; end
41
+ class ResourceExhausted < Error; end
42
+ class Unauthenticated < Error; end
43
+ class Unavailable < Error; end
44
+ class Unimplemented < Error; end
45
+ class Unknown < Error; end
46
+ end
47
+ end
48
+ end
data/lib/gruf/client.rb CHANGED
@@ -15,9 +15,6 @@
15
15
  # COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
16
16
  # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
17
17
  #
18
- require_relative 'client/error'
19
- require_relative 'client/error_factory'
20
-
21
18
  module Gruf
22
19
  ##
23
20
  # Abstracts out the calling interface for interacting with gRPC clients. Streamlines calling and provides
@@ -165,14 +165,15 @@ module Gruf
165
165
  determine_loggers
166
166
  self.ssl_crt_file = "#{root_path}config/ssl/#{environment}.crt"
167
167
  self.ssl_key_file = "#{root_path}config/ssl/#{environment}.key"
168
- self.controllers_path = root_path.to_s.empty? ? 'app/rpc' : "#{root_path}/app/rpc"
168
+ cp = ::ENV.fetch('GRUF_CONTROLLERS_PATH', 'app/rpc').to_s
169
+ self.controllers_path = root_path.to_s.empty? ? cp : "#{root_path}/#{cp}"
169
170
  self.backtrace_on_error = ::ENV.fetch('GRPC_BACKTRACE_ON_ERROR', 0).to_i.positive?
170
171
  self.rpc_server_options = {
171
172
  max_waiting_requests: ::ENV.fetch('GRPC_SERVER_MAX_WAITING_REQUESTS',
172
173
  GRPC::RpcServer::DEFAULT_MAX_WAITING_REQUESTS).to_i,
173
174
  pool_size: ::ENV.fetch('GRPC_SERVER_POOL_SIZE', GRPC::RpcServer::DEFAULT_POOL_SIZE).to_i,
174
- pool_keep_alive: ::ENV.fetch('GRPC_SERVER_POOL_KEEP_ALIVE', GRPC::Pool::DEFAULT_KEEP_ALIVE),
175
- poll_period: ::ENV.fetch('GRPC_SERVER_POLL_PERIOD', GRPC::RpcServer::DEFAULT_POLL_PERIOD),
175
+ pool_keep_alive: ::ENV.fetch('GRPC_SERVER_POOL_KEEP_ALIVE', GRPC::Pool::DEFAULT_KEEP_ALIVE).to_i,
176
+ poll_period: ::ENV.fetch('GRPC_SERVER_POLL_PERIOD', GRPC::RpcServer::DEFAULT_POLL_PERIOD).to_i,
176
177
  connect_md_proc: nil,
177
178
  server_args: {}
178
179
  }
@@ -183,6 +184,13 @@ module Gruf
183
184
  options
184
185
  end
185
186
 
187
+ ##
188
+ # @return [Boolean]
189
+ #
190
+ def development?
191
+ environment == 'development'
192
+ end
193
+
186
194
  private
187
195
 
188
196
  ##
@@ -194,7 +202,7 @@ module Gruf
194
202
  if defined?(::Rails)
195
203
  ::Rails.env.to_s
196
204
  else
197
- (ENV['RACK_ENV'] || ENV['RAILS_ENV'] || 'development').to_s
205
+ ENV.fetch('RACK_ENV') { ENV.fetch('RAILS_ENV', 'development') }.to_s
198
206
  end
199
207
  end
200
208
 
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright (c) 2017-present, BigCommerce Pty. Ltd. All rights reserved
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
6
+ # documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
7
+ # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit
8
+ # persons to whom the Software is furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
11
+ # Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
14
+ # WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
15
+ # COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
16
+ # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
17
+ #
18
+ module Gruf
19
+ module Controllers
20
+ ##
21
+ # Handles autoloading of Gruf controllers in the application path. This allows for code reloading on Gruf
22
+ # controllers.
23
+ #
24
+ class Autoloader
25
+ include ::Gruf::Loggable
26
+
27
+ # @!attribute [r] path
28
+ # @return [String] The path for this autoloader
29
+ attr_reader :path
30
+
31
+ ##
32
+ # @param [String] path
33
+ # @param [Boolean] reloading
34
+ # @param [String] tag
35
+ #
36
+ def initialize(path:, reloading: nil, tag: nil)
37
+ super()
38
+ @path = path
39
+ @loader = ::Zeitwerk::Loader.new
40
+ @loader.tag = tag || 'gruf-controllers'
41
+ @setup = false
42
+ @reloading_enabled = reloading || ::Gruf.development?
43
+ setup!
44
+ end
45
+
46
+ ##
47
+ # Reload all files managed by the autoloader, if reloading is enabled
48
+ #
49
+ def reload
50
+ @loader.reload if @reloading_enabled
51
+ end
52
+
53
+ private
54
+
55
+ ##
56
+ # @return [Boolean]
57
+ #
58
+ def setup!
59
+ return true if @setup
60
+
61
+ return false unless File.directory?(@path)
62
+
63
+ @loader.enable_reloading if @reloading_enabled
64
+ @loader.ignore("#{@path}/**/*_pb.rb")
65
+ @loader.push_dir(@path)
66
+ @loader.setup
67
+ # always eager load RPC files, so that the service binder can bind the Gruf::Controller instantiation
68
+ # to the gRPC Service classes
69
+ @loader.eager_load
70
+ @setup = true
71
+ end
72
+ end
73
+ end
74
+ end
@@ -15,9 +15,6 @@
15
15
  # COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
16
16
  # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
17
17
  #
18
- require_relative 'request'
19
- require_relative 'service_binder'
20
-
21
18
  module Gruf
22
19
  module Controllers
23
20
  ##
@@ -67,11 +64,12 @@ module Gruf
67
64
  #
68
65
  def self.bind(service)
69
66
  service_class = service.name.constantize
70
- Gruf.services << service_class
67
+ ::Gruf.logger.debug "[gruf] Binding #{service_class} to #{name}"
68
+ ::Gruf.services << service_class
71
69
  # rubocop:disable ThreadSafety/InstanceVariableInClassMethod
72
70
  @bound_service = service_class
73
71
  # rubocop:enable ThreadSafety/InstanceVariableInClassMethod
74
- ServiceBinder.new(service_class).bind!(self)
72
+ ServiceBinder.bind!(service: service_class, controller: self)
75
73
  end
76
74
 
77
75
  ##
@@ -92,6 +90,7 @@ module Gruf
92
90
  # @param [block] &block The passed block for executing the method
93
91
  #
94
92
  def call(method_key, &block)
93
+ ::Gruf.autoloaders.reload if ::Gruf.development?
95
94
  Interceptors::Context.new(@interceptors).intercept! do
96
95
  process_action(method_key, &block)
97
96
  end
@@ -26,92 +26,79 @@ module Gruf
26
26
  #
27
27
  class BoundDesc < SimpleDelegator; end
28
28
 
29
- ##
30
- # Initialize a service binder instance with the given service
31
- #
32
- # @param [GRPC::GenericService] service The gRPC service stub to bind
33
- #
34
- def initialize(service)
35
- @service = service
36
- end
37
-
38
- ##
39
- # Bind all methods on the service to the passed controller
40
- #
41
- # @param [Class<Gruf::Controllers::Base>] controller
42
- #
43
- def bind!(controller)
44
- rpc_methods.each { |name, desc| bind_method(controller, name, desc) }
45
- end
46
-
47
- private
48
-
49
- ##
50
- # Bind the grpc methods to the service, allowing for server interception and execution control
51
- #
52
- # @param [Gruf::Controllers::Base] controller
53
- # @param [Symbol] method_name
54
- # @param [BoundDesc] desc
55
- #
56
- def bind_method(controller, method_name, desc)
57
- method_key = method_name.to_s.underscore.to_sym
58
- service_ref = @service
29
+ class << self
30
+ ##
31
+ # Bind all methods on the service to the passed controller
32
+ #
33
+ # @param [Class<Gruf::Controllers::Base>] controller
34
+ #
35
+ def bind!(service:, controller:)
36
+ rpc_methods = service.rpc_descs.map { |rd| BoundDesc.new(rd) }
37
+ rpc_methods.each { |name, desc| bind_method(service, controller, name, desc) }
38
+ end
59
39
 
60
- @service.class_eval do
61
- if desc.request_response?
62
- define_method(method_key) do |message, active_call|
63
- c = controller.new(
64
- method_key: method_key,
65
- service: service_ref,
66
- message: message,
67
- active_call: active_call,
68
- rpc_desc: desc
69
- )
70
- c.call(method_key)
71
- end
72
- elsif desc.client_streamer?
73
- define_method(method_key) do |active_call|
74
- c = controller.new(
75
- method_key: method_key,
76
- service: service_ref,
77
- message: proc { |&block| active_call.each_remote_read(&block) },
78
- active_call: active_call,
79
- rpc_desc: desc
80
- )
81
- c.call(method_key)
82
- end
83
- elsif desc.server_streamer?
84
- define_method(method_key) do |message, active_call, &block|
85
- c = controller.new(
86
- method_key: method_key,
87
- service: service_ref,
88
- message: message,
89
- active_call: active_call,
90
- rpc_desc: desc
91
- )
92
- c.call(method_key, &block)
93
- end
94
- else # bidi
95
- define_method(method_key) do |messages, active_call, &block|
96
- c = controller.new(
97
- method_key: method_key,
98
- service: service_ref,
99
- message: messages,
100
- active_call: active_call,
101
- rpc_desc: desc
102
- )
103
- c.call(method_key, &block)
40
+ ##
41
+ # Bind the grpc methods to the service, allowing for server interception and execution control
42
+ #
43
+ # @param [Gruf::Controllers::Base] controller
44
+ # @param [Symbol] method_name
45
+ # @param [BoundDesc] desc
46
+ #
47
+ def bind_method(service_ref, controller, method_name, desc)
48
+ method_key = method_name.to_s.underscore.to_sym
49
+ service_ref.class_eval do
50
+ if desc.request_response?
51
+ define_method(method_key) do |message, active_call|
52
+ controller = controller.name.constantize
53
+ c = controller.new(
54
+ method_key: method_key,
55
+ service: service_ref,
56
+ message: message,
57
+ active_call: active_call,
58
+ rpc_desc: desc
59
+ )
60
+ c.call(method_key)
61
+ end
62
+ elsif desc.client_streamer?
63
+ define_method(method_key) do |active_call|
64
+ controller = controller.name.constantize
65
+ c = controller.new(
66
+ method_key: method_key,
67
+ service: service_ref,
68
+ message: proc { |&block| active_call.each_remote_read(&block) },
69
+ active_call: active_call,
70
+ rpc_desc: desc
71
+ )
72
+ c.call(method_key)
73
+ end
74
+ elsif desc.server_streamer?
75
+ define_method(method_key) do |message, active_call, &block|
76
+ controller = controller.name.constantize
77
+ c = controller.new(
78
+ method_key: method_key,
79
+ service: service_ref,
80
+ message: message,
81
+ active_call: active_call,
82
+ rpc_desc: desc
83
+ )
84
+ c.call(method_key, &block)
85
+ end
86
+ else # bidi
87
+ define_method(method_key) do |messages, active_call, &block|
88
+ controller = controller.name.constantize
89
+ c = controller.new(
90
+ method_key: method_key,
91
+ service: service_ref,
92
+ message: messages,
93
+ active_call: active_call,
94
+ rpc_desc: desc
95
+ )
96
+ c.call(method_key, &block)
97
+ end
104
98
  end
105
99
  end
106
100
  end
107
101
  end
108
-
109
- ##
110
- # @return Array<Gruf::Controllers::ServiceBinder::BoundDesc>
111
- #
112
- def rpc_methods
113
- @service.rpc_descs.map { |rd| BoundDesc.new(rd) }
114
- end
115
102
  end
116
103
  end
117
104
  end
data/lib/gruf/error.rb CHANGED
@@ -15,10 +15,6 @@
15
15
  # COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
16
16
  # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
17
17
  #
18
- require_relative 'errors/field'
19
- require_relative 'errors/debug_info'
20
- require_relative 'serializers/errors/base'
21
- require_relative 'serializers/errors/json'
22
18
 
23
19
  module Gruf
24
20
  ##
@@ -16,20 +16,6 @@
16
16
  # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
17
17
  #
18
18
  module Gruf
19
- ##
20
- # Handles internal gruf logging requests
21
- #
22
- module Logger
23
- ##
24
- # Return the current Gruf logger
25
- #
26
- # @return [Logger]
27
- #
28
- def logger
29
- Gruf.logger
30
- end
31
- end
32
-
33
19
  ##
34
20
  # Handles grpc internal logging requests
35
21
  #
@@ -1,9 +1,39 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # Copyright (c) 2017-present, BigCommerce Pty. Ltd. All rights reserved
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
6
+ # documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
7
+ # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit
8
+ # persons to whom the Software is furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
11
+ # Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
14
+ # WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
15
+ # COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
16
+ # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
17
+ #
3
18
  module Gruf
4
19
  module Integrations
5
20
  module Rails
21
+ ##
22
+ # Rails integration for Gruf, that currently only manages code autoloading in a Rails context
23
+ #
6
24
  class Railtie < ::Rails::Railtie
25
+ initializer 'gruf.initializer' do |app|
26
+ config.before_configuration do
27
+ # Remove autoloading of the controllers path from Rails' zeitwerk, so that we ensure Gruf's zeitwerk
28
+ # properly manages them itself. This allows us to manage code reloading and logging in Gruf specifically
29
+ app.config.eager_load_paths -= [::Gruf.controllers_path] if app.config.respond_to?(:eager_load_paths)
30
+ if ::Rails.respond_to?(:autoloaders) # if we're on a late enough version of rails
31
+ ::Rails.autoloaders.each do |autoloader|
32
+ autoloader.ignore(Gruf.controllers_path)
33
+ end
34
+ end
35
+ end
36
+ end
7
37
  end
8
38
  end
9
39
  end
@@ -44,13 +44,3 @@ module Gruf
44
44
  end
45
45
  end
46
46
  end
47
-
48
- require_relative 'client_interceptor'
49
- require_relative 'server_interceptor'
50
- require_relative 'context'
51
- require_relative 'timer'
52
- require_relative 'active_record/connection_reset'
53
- require_relative 'authentication/basic'
54
- require_relative 'instrumentation/statsd'
55
- require_relative 'instrumentation/output_metadata_timer'
56
- require_relative 'instrumentation/request_logging/interceptor'
@@ -16,9 +16,6 @@
16
16
  # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
17
17
  #
18
18
  require 'socket'
19
- require_relative 'formatters/base'
20
- require_relative 'formatters/logstash'
21
- require_relative 'formatters/plain'
22
19
 
23
20
  module Gruf
24
21
  module Interceptors
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright (c) 2017-present, BigCommerce Pty. Ltd. All rights reserved
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
6
+ # documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
7
+ # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit
8
+ # persons to whom the Software is furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
11
+ # Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
14
+ # WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
15
+ # COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
16
+ # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
17
+ #
18
+ module Gruf
19
+ ##
20
+ # Handles internal gruf logging requests
21
+ #
22
+ module Logger
23
+ ##
24
+ # Return the current Gruf logger
25
+ #
26
+ # @return [Logger]
27
+ #
28
+ def logger
29
+ Gruf.logger
30
+ end
31
+ end
32
+ end
data/lib/gruf/server.rb CHANGED
@@ -43,39 +43,45 @@ module Gruf
43
43
  @options = opts || {}
44
44
  @interceptors = opts.fetch(:interceptor_registry, Gruf.interceptors)
45
45
  @interceptors = Gruf::Interceptors::Registry.new unless @interceptors.is_a?(Gruf::Interceptors::Registry)
46
- @services = []
46
+ @services = nil
47
47
  @started = false
48
48
  @hostname = opts.fetch(:hostname, Gruf.server_binding_url)
49
49
  @event_listener_proc = opts.fetch(:event_listener_proc, Gruf.event_listener_proc)
50
- setup
51
50
  end
52
51
 
53
52
  ##
54
53
  # @return [GRPC::RpcServer] The GRPC server running
55
54
  #
56
55
  def server
57
- @server ||= begin
58
- # For backward compatibility, we allow these options to be passed directly
59
- # in the Gruf::Server options, or via Gruf.rpc_server_options.
60
- server_options = {
61
- pool_size: options.fetch(:pool_size, Gruf.rpc_server_options[:pool_size]),
62
- max_waiting_requests: options.fetch(:max_waiting_requests, Gruf.rpc_server_options[:max_waiting_requests]),
63
- poll_period: options.fetch(:poll_period, Gruf.rpc_server_options[:poll_period]),
64
- pool_keep_alive: options.fetch(:pool_keep_alive, Gruf.rpc_server_options[:pool_keep_alive]),
65
- connect_md_proc: options.fetch(:connect_md_proc, Gruf.rpc_server_options[:connect_md_proc]),
66
- server_args: options.fetch(:server_args, Gruf.rpc_server_options[:server_args])
67
- }
68
-
69
- server = if @event_listener_proc
70
- server_options[:event_listener_proc] = @event_listener_proc
71
- Gruf::InstrumentableGrpcServer.new(**server_options)
72
- else
73
- GRPC::RpcServer.new(**server_options)
74
- end
75
-
76
- @port = server.add_http2_port(@hostname, ssl_credentials)
77
- @services.each { |s| server.handle(s) }
78
- server
56
+ server_mutex do
57
+ @server ||= begin
58
+ # For backward compatibility, we allow these options to be passed directly
59
+ # in the Gruf::Server options, or via Gruf.rpc_server_options.
60
+ server_options = {
61
+ pool_size: options.fetch(:pool_size, Gruf.rpc_server_options[:pool_size]),
62
+ max_waiting_requests: options.fetch(:max_waiting_requests, Gruf.rpc_server_options[:max_waiting_requests]),
63
+ poll_period: options.fetch(:poll_period, Gruf.rpc_server_options[:poll_period]),
64
+ pool_keep_alive: options.fetch(:pool_keep_alive, Gruf.rpc_server_options[:pool_keep_alive]),
65
+ connect_md_proc: options.fetch(:connect_md_proc, Gruf.rpc_server_options[:connect_md_proc]),
66
+ server_args: options.fetch(:server_args, Gruf.rpc_server_options[:server_args])
67
+ }
68
+
69
+ server = if @event_listener_proc
70
+ server_options[:event_listener_proc] = @event_listener_proc
71
+ Gruf::InstrumentableGrpcServer.new(**server_options)
72
+ else
73
+ GRPC::RpcServer.new(**server_options)
74
+ end
75
+
76
+ @port = server.add_http2_port(@hostname, ssl_credentials)
77
+ # do not reference `services` any earlier than this method, as it allows autoloading to take effect
78
+ # and load services into `Gruf.services` as late as possible, which gives us flexibility with different
79
+ # execution paths (such as vanilla ruby, grape, multiple Rails versions, etc). The autoloaders are
80
+ # initially loaded in `Gruf::Cli::Executor` _directly_ before the gRPC services are loaded into the gRPC
81
+ # server, to allow for loading services as late as possible in the execution chain.
82
+ services.each { |s| server.handle(s) }
83
+ server
84
+ end
79
85
  end
80
86
  end
81
87
 
@@ -87,7 +93,7 @@ module Gruf
87
93
  update_proc_title(:starting)
88
94
 
89
95
  server_thread = Thread.new do
90
- logger.info { "Starting gruf server at #{@hostname}..." }
96
+ logger.info { "[gruf] Starting gruf server at #{@hostname}..." }
91
97
  server.run_till_terminated_or_interrupted(KILL_SIGNALS)
92
98
  end
93
99
  @started = true
@@ -96,7 +102,7 @@ module Gruf
96
102
  @started = false
97
103
 
98
104
  update_proc_title(:stopped)
99
- logger.info { 'Goodbye!' }
105
+ logger.info { '[gruf] Goodbye!' }
100
106
  end
101
107
  # :nocov:
102
108
 
@@ -109,7 +115,7 @@ module Gruf
109
115
  def add_service(klass)
110
116
  raise ServerAlreadyStartedError if @started
111
117
 
112
- @services << klass unless @services.include?(klass)
118
+ @services << klass unless services.include?(klass)
113
119
  end
114
120
 
115
121
  ##
@@ -183,31 +189,11 @@ module Gruf
183
189
  private
184
190
 
185
191
  ##
186
- # Setup server
187
- #
188
- # :nocov:
189
- def setup
190
- load_controllers
191
- end
192
- # :nocov:
193
-
194
- ##
195
- # Auto-load all gRPC handlers
192
+ # @return [Array<Class>]
196
193
  #
197
- # :nocov:
198
- def load_controllers
199
- return unless File.directory?(controllers_path)
200
-
201
- path = File.realpath(controllers_path)
202
- $LOAD_PATH.unshift(path)
203
- Dir["#{path}/**/*.rb"].each do |f|
204
- next if f.include?('_pb') # exclude if people include proto generated files in app/rpc
205
-
206
- logger.info "- Loading gRPC service file: #{f}"
207
- load File.realpath(f)
208
- end
194
+ def services
195
+ @services ||= (::Gruf.services || (options.fetch(:services, nil) || []))
209
196
  end
210
- # :nocov:
211
197
 
212
198
  ##
213
199
  # @param [String]
@@ -242,5 +228,17 @@ module Gruf
242
228
  Process.setproctitle("gruf #{Gruf::VERSION} -- #{state}")
243
229
  end
244
230
  # :nocov:
231
+ #
232
+
233
+ ##
234
+ # Handle thread-safe access to the server
235
+ #
236
+ def server_mutex(&block)
237
+ @server_mutex ||= begin
238
+ require 'monitor'
239
+ Monitor.new
240
+ end
241
+ @server_mutex.synchronize(&block)
242
+ end
245
243
  end
246
244
  end
data/lib/gruf/version.rb CHANGED
@@ -16,5 +16,5 @@
16
16
  # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
17
17
  #
18
18
  module Gruf
19
- VERSION = '2.14.0'
19
+ VERSION = '2.15.1'
20
20
  end
data/lib/gruf.rb CHANGED
@@ -20,26 +20,16 @@ require 'active_support/core_ext/module/delegation'
20
20
  require 'active_support/concern'
21
21
  require 'active_support/inflector'
22
22
  require 'base64'
23
- require_relative 'gruf/version'
24
- require_relative 'gruf/logging'
25
- require_relative 'gruf/loggable'
26
- require_relative 'gruf/configuration'
27
- require_relative 'gruf/errors/helpers'
28
- require_relative 'gruf/cli/executor'
29
- require_relative 'gruf/controllers/base'
30
- require_relative 'gruf/outbound/request_context'
31
- require_relative 'gruf/interceptors/registry'
32
- require_relative 'gruf/interceptors/base'
33
- require_relative 'gruf/hooks/registry'
34
- require_relative 'gruf/hooks/executor'
35
- require_relative 'gruf/hooks/base'
36
- require_relative 'gruf/timer'
37
- require_relative 'gruf/response'
38
- require_relative 'gruf/error'
39
- require_relative 'gruf/client'
40
- require_relative 'gruf/synchronized_client'
41
- require_relative 'gruf/instrumentable_grpc_server'
42
- require_relative 'gruf/server'
23
+
24
+ # use Zeitwerk to lazily autoload all the files in the lib directory
25
+ require 'zeitwerk'
26
+ loader = ::Zeitwerk::Loader.new
27
+ loader.tag = File.basename(__FILE__, '.rb')
28
+ loader.inflector = ::Zeitwerk::GemInflector.new(__FILE__)
29
+ loader.ignore("#{__dir__}/gruf/integrations/rails/railtie.rb")
30
+ loader.push_dir(__dir__)
31
+ loader.setup
32
+
43
33
  require_relative 'gruf/integrations/rails/railtie' if defined?(::Rails)
44
34
 
45
35
  ##
@@ -47,4 +37,10 @@ require_relative 'gruf/integrations/rails/railtie' if defined?(::Rails)
47
37
  #
48
38
  module Gruf
49
39
  extend Configuration
40
+
41
+ class << self
42
+ def autoloaders
43
+ Autoloaders
44
+ end
45
+ end
50
46
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: gruf
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.14.0
4
+ version: 2.15.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Shaun McCormick
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-04-08 00:00:00.000000000 Z
11
+ date: 2022-07-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler-audit
@@ -304,6 +304,20 @@ dependencies:
304
304
  - - ">="
305
305
  - !ruby/object:Gem::Version
306
306
  version: '0.1'
307
+ - !ruby/object:Gem::Dependency
308
+ name: zeitwerk
309
+ requirement: !ruby/object:Gem::Requirement
310
+ requirements:
311
+ - - ">="
312
+ - !ruby/object:Gem::Version
313
+ version: '2'
314
+ type: :runtime
315
+ prerelease: false
316
+ version_requirements: !ruby/object:Gem::Requirement
317
+ requirements:
318
+ - - ">="
319
+ - !ruby/object:Gem::Version
320
+ version: '2'
307
321
  description: gRPC Ruby Framework for building complex gRPC applications at scale
308
322
  email:
309
323
  - splittingred@gmail.com
@@ -318,11 +332,14 @@ files:
318
332
  - bin/gruf
319
333
  - gruf.gemspec
320
334
  - lib/gruf.rb
335
+ - lib/gruf/autoloaders.rb
321
336
  - lib/gruf/cli/executor.rb
322
337
  - lib/gruf/client.rb
323
338
  - lib/gruf/client/error.rb
324
339
  - lib/gruf/client/error_factory.rb
340
+ - lib/gruf/client/errors.rb
325
341
  - lib/gruf/configuration.rb
342
+ - lib/gruf/controllers/autoloader.rb
326
343
  - lib/gruf/controllers/base.rb
327
344
  - lib/gruf/controllers/request.rb
328
345
  - lib/gruf/controllers/service_binder.rb
@@ -330,6 +347,7 @@ files:
330
347
  - lib/gruf/errors/debug_info.rb
331
348
  - lib/gruf/errors/field.rb
332
349
  - lib/gruf/errors/helpers.rb
350
+ - lib/gruf/grpc_logger.rb
333
351
  - lib/gruf/hooks/base.rb
334
352
  - lib/gruf/hooks/executor.rb
335
353
  - lib/gruf/hooks/registry.rb
@@ -350,7 +368,7 @@ files:
350
368
  - lib/gruf/interceptors/server_interceptor.rb
351
369
  - lib/gruf/interceptors/timer.rb
352
370
  - lib/gruf/loggable.rb
353
- - lib/gruf/logging.rb
371
+ - lib/gruf/logger.rb
354
372
  - lib/gruf/outbound/request_context.rb
355
373
  - lib/gruf/response.rb
356
374
  - lib/gruf/serializers/errors/base.rb
@@ -363,7 +381,12 @@ homepage: https://github.com/bigcommerce/gruf
363
381
  licenses:
364
382
  - MIT
365
383
  metadata:
384
+ bug_tracker_uri: https://github.com/bigcommerce/gruf/issues
385
+ changelog_uri: https://github.com/bigcommerce/gruf/CHANGELOG.md
386
+ homepage_uri: https://github.com/bigcommerce/gruf
366
387
  rubygems_mfa_required: 'true'
388
+ source_code_uri: https://github.com/bigcommerce/gruf
389
+ wiki_uri: https://github.com/bigcommerce/gruf/wiki
367
390
  post_install_message:
368
391
  rdoc_options: []
369
392
  require_paths:
@@ -382,7 +405,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
382
405
  - !ruby/object:Gem::Version
383
406
  version: '0'
384
407
  requirements: []
385
- rubygems_version: 3.3.7
408
+ rubygems_version: 3.3.12
386
409
  signing_key:
387
410
  specification_version: 4
388
411
  summary: gRPC Ruby Framework