gruf 2.14.0 → 2.15.1

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 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