gruf 2.2.2 → 2.9.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.
Files changed (49) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +107 -12
  3. data/CODE_OF_CONDUCT.md +38 -41
  4. data/README.md +8 -360
  5. data/bin/gruf +2 -12
  6. data/gruf.gemspec +31 -7
  7. data/lib/gruf/cli/executor.rb +102 -0
  8. data/lib/gruf/client/error.rb +68 -0
  9. data/lib/gruf/client/error_factory.rb +105 -0
  10. data/lib/gruf/client.rb +52 -38
  11. data/lib/gruf/configuration.rb +25 -5
  12. data/lib/gruf/controllers/base.rb +35 -12
  13. data/lib/gruf/controllers/request.rb +21 -10
  14. data/lib/gruf/controllers/service_binder.rb +38 -6
  15. data/lib/gruf/error.rb +34 -9
  16. data/lib/gruf/errors/debug_info.rb +9 -2
  17. data/lib/gruf/errors/field.rb +2 -0
  18. data/lib/gruf/errors/helpers.rb +5 -0
  19. data/lib/gruf/hooks/base.rb +34 -0
  20. data/lib/gruf/hooks/executor.rb +47 -0
  21. data/lib/gruf/hooks/registry.rb +159 -0
  22. data/lib/gruf/instrumentable_grpc_server.rb +64 -0
  23. data/lib/gruf/integrations/rails/railtie.rb +10 -0
  24. data/lib/gruf/interceptors/active_record/connection_reset.rb +4 -3
  25. data/lib/gruf/interceptors/authentication/basic.rb +10 -2
  26. data/lib/gruf/interceptors/base.rb +3 -0
  27. data/lib/gruf/interceptors/client_interceptor.rb +117 -0
  28. data/lib/gruf/interceptors/context.rb +6 -4
  29. data/lib/gruf/interceptors/instrumentation/output_metadata_timer.rb +5 -4
  30. data/lib/gruf/interceptors/instrumentation/request_logging/formatters/base.rb +5 -1
  31. data/lib/gruf/interceptors/instrumentation/request_logging/formatters/logstash.rb +5 -1
  32. data/lib/gruf/interceptors/instrumentation/request_logging/formatters/plain.rb +5 -1
  33. data/lib/gruf/interceptors/instrumentation/request_logging/interceptor.rb +60 -29
  34. data/lib/gruf/interceptors/instrumentation/statsd.rb +5 -4
  35. data/lib/gruf/interceptors/registry.rb +6 -1
  36. data/lib/gruf/interceptors/server_interceptor.rb +2 -0
  37. data/lib/gruf/interceptors/timer.rb +12 -2
  38. data/lib/gruf/loggable.rb +2 -0
  39. data/lib/gruf/logging.rb +2 -0
  40. data/lib/gruf/outbound/request_context.rb +71 -0
  41. data/lib/gruf/response.rb +5 -2
  42. data/lib/gruf/serializers/errors/base.rb +2 -0
  43. data/lib/gruf/serializers/errors/json.rb +2 -0
  44. data/lib/gruf/server.rb +57 -23
  45. data/lib/gruf/synchronized_client.rb +97 -0
  46. data/lib/gruf/timer.rb +9 -6
  47. data/lib/gruf/version.rb +3 -1
  48. data/lib/gruf.rb +10 -0
  49. metadata +254 -17
data/gruf.gemspec CHANGED
@@ -1,4 +1,5 @@
1
- # coding: utf-8
1
+ # frozen_string_literal: true
2
+
2
3
  # Copyright (c) 2017-present, BigCommerce Pty. Ltd. All rights reserved
3
4
  #
4
5
  # Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
@@ -14,7 +15,7 @@
14
15
  # COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
15
16
  # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
16
17
  #
17
- $:.push File.expand_path("../lib", __FILE__)
18
+ $LOAD_PATH.push File.expand_path('lib', __dir__)
18
19
  require 'gruf/version'
19
20
 
20
21
  Gem::Specification.new do |spec|
@@ -25,18 +26,41 @@ Gem::Specification.new do |spec|
25
26
  spec.licenses = ['MIT']
26
27
 
27
28
  spec.summary = 'gRPC Ruby Framework'
28
- spec.description = spec.summary
29
+ spec.description = 'gRPC Ruby Framework for building complex gRPC applications at scale'
29
30
  spec.homepage = 'https://github.com/bigcommerce/gruf'
30
31
 
31
32
  spec.files = Dir['README.md', 'CHANGELOG.md', 'CODE_OF_CONDUCT.md', 'lib/**/*', 'gruf.gemspec']
32
- spec.executables << 'gruf'
33
+ spec.executables << 'gruf'
33
34
  spec.require_paths = ['lib']
34
35
 
36
+ spec.required_ruby_version = '>= 2.4', '< 3.1'
37
+
35
38
  spec.add_development_dependency 'bundler', '~> 1.11'
36
- spec.add_development_dependency 'rake', '~> 10.0'
37
- spec.add_development_dependency 'pry'
39
+ spec.add_development_dependency 'bundler-audit', '>= 0.6'
40
+ # rubocop:disable Gemspec/RubyVersionGlobalsUsage
41
+ spec.add_development_dependency(
42
+ 'factory_bot',
43
+ (Gem::Version.new(RUBY_VERSION) >= Gem::Version.new('2.5') ? '>= 6.1' : '~> 5.2')
44
+ )
45
+ # rubocop:enable Gemspec/RubyVersionGlobalsUsage
46
+ spec.add_development_dependency 'ffaker', '>= 2.15'
47
+ spec.add_development_dependency 'pry', '~> 0.12'
48
+ spec.add_development_dependency 'pry-byebug', '>= 3.9'
49
+ spec.add_development_dependency 'rake', '>= 10.0'
50
+ spec.add_development_dependency 'rspec', '>= 3.8'
51
+ spec.add_development_dependency 'rspec_junit_formatter', '>= 0.4'
52
+ spec.add_development_dependency 'rubocop', '>= 1.0'
53
+ spec.add_development_dependency 'rubocop-performance', '>= 0.0.1'
54
+ spec.add_development_dependency 'rubocop-rspec', '>= 2.0'
55
+ spec.add_development_dependency 'rubocop-thread_safety', '>= 0.3'
56
+ spec.add_development_dependency 'simplecov', '>= 0.16'
38
57
 
58
+ spec.add_runtime_dependency 'activesupport', '> 4'
59
+ spec.add_runtime_dependency 'concurrent-ruby', '> 1'
60
+ spec.add_runtime_dependency 'e2mmap', '~> 0.1'
39
61
  spec.add_runtime_dependency 'grpc', '~> 1.10'
40
62
  spec.add_runtime_dependency 'grpc-tools', '~> 1.10'
41
- spec.add_runtime_dependency 'activesupport'
63
+ spec.add_runtime_dependency 'json', '>= 2.3'
64
+ spec.add_runtime_dependency 'slop', '~> 4.6'
65
+ spec.add_runtime_dependency 'thwait', '~> 0.1'
42
66
  end
@@ -0,0 +1,102 @@
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
+ require 'slop'
19
+
20
+ module Gruf
21
+ module Cli
22
+ ##
23
+ # Handles execution of the gruf binstub, along with command-line arguments
24
+ #
25
+ class Executor
26
+ ##
27
+ # @param [Hash|ARGV]
28
+ #
29
+ def initialize(
30
+ args = ARGV,
31
+ server: nil,
32
+ services: nil,
33
+ hook_executor: nil,
34
+ logger: nil
35
+ )
36
+ @args = args
37
+ setup! # ensure we set some defaults from CLI here so we can allow configuration
38
+ @services = services || Gruf.services
39
+ @hook_executor = hook_executor || Gruf::Hooks::Executor.new(hooks: Gruf.hooks&.prepare)
40
+ @server = server || Gruf::Server.new(Gruf.server_options)
41
+ @logger = logger || Gruf.logger || ::Logger.new($stderr)
42
+ end
43
+
44
+ ##
45
+ # Run the server
46
+ #
47
+ def run
48
+ exception = nil
49
+ begin
50
+ @services.each { |s| @server.add_service(s) }
51
+ @hook_executor.call(:before_server_start, server: @server)
52
+ @server.start!
53
+ rescue StandardError => e
54
+ exception = e
55
+ # Catch the exception here so that we always ensure the post hook runs
56
+ # This allows systems wanting to provide external server instrumentation
57
+ # the ability to properly handle server failures
58
+ @logger.fatal("FATAL ERROR: #{e.message} #{e.backtrace.join("\n")}")
59
+ end
60
+ @hook_executor.call(:after_server_stop, server: @server)
61
+ raise exception if exception
62
+ end
63
+
64
+ private
65
+
66
+ ##
67
+ # Setup options for CLI execution and configure Gruf based on inputs
68
+ #
69
+ def setup!
70
+ opts = parse_options
71
+
72
+ Gruf.server_binding_url = opts[:host] if opts[:host]
73
+ if opts.suppress_default_interceptors?
74
+ Gruf.interceptors.remove(Gruf::Interceptors::ActiveRecord::ConnectionReset)
75
+ Gruf.interceptors.remove(Gruf::Interceptors::Instrumentation::OutputMetadataTimer)
76
+ end
77
+ Gruf.backtrace_on_error = true if opts.backtrace_on_error?
78
+ end
79
+
80
+ ##
81
+ # Parse all CLI arguments into an options result
82
+ #
83
+ # @return [Slop::Result]
84
+ #
85
+ def parse_options
86
+ ::Slop.parse(@args) do |o|
87
+ o.null '-h', '--help', 'Display help message' do
88
+ puts o
89
+ exit(0)
90
+ end
91
+ o.string '--host', 'Specify the binding url for the gRPC service'
92
+ o.bool '--suppress-default-interceptors', 'Do not use the default interceptors'
93
+ o.bool '--backtrace-on-error', 'Push backtraces on exceptions to the error serializer'
94
+ o.null '-v', '--version', 'print gruf version' do
95
+ puts Gruf::VERSION
96
+ exit(0)
97
+ end
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,68 @@
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
+ # Represents an error that was returned from the server's trailing metadata. Used as a custom exception object
22
+ # that is instead raised in the case of the service returning serialized error data, as opposed to the normal
23
+ # GRPC::BadStatus error
24
+ #
25
+ class Error < StandardError
26
+ # @return [Object] error The deserialized error
27
+ attr_reader :error
28
+
29
+ ##
30
+ # Initialize the client error
31
+ #
32
+ # @param [Object] error The deserialized error
33
+ #
34
+ def initialize(error)
35
+ @error = error
36
+ super
37
+ end
38
+ end
39
+
40
+ ##
41
+ # See https://github.com/grpc/grpc-go/blob/master/codes/codes.go for a detailed summary of each error type
42
+ #
43
+ module Errors
44
+ class Base < Gruf::Client::Error; end
45
+ class Error < Base; end
46
+ class Validation < Base; end
47
+
48
+ class Ok < Base; end
49
+
50
+ class InvalidArgument < Validation; end
51
+ class NotFound < Validation; end
52
+ class AlreadyExists < Validation; end
53
+ class OutOfRange < Validation; end
54
+
55
+ class Cancelled < Error; end
56
+ class DataLoss < Error; end
57
+ class DeadlineExceeded < Error; end
58
+ class FailedPrecondition < Error; end
59
+ class Internal < Error; end
60
+ class PermissionDenied < Error; end
61
+ class ResourceExhausted < Error; end
62
+ class Unauthenticated < Error; end
63
+ class Unavailable < Error; end
64
+ class Unimplemented < Error; end
65
+ class Unknown < Error; end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,105 @@
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
+ # Translates exceptions into Gruf::Client::Errors
22
+ #
23
+ class ErrorFactory
24
+ ##
25
+ # @param [Class] default_class
26
+ # @param [Class] deserializer_class
27
+ # @param [String|Symbol] metadata_key
28
+ #
29
+ def initialize(
30
+ default_class: nil,
31
+ deserializer_class: nil,
32
+ metadata_key: nil
33
+ )
34
+ @default_class = default_class || Gruf::Client::Errors::Internal
35
+ @metadata_key = (metadata_key || Gruf.error_metadata_key).to_s
36
+ @deserializer_class = deserializer_class || default_serializer
37
+ end
38
+
39
+ ##
40
+ # Determine the proper error class to raise given the incoming exception. This will attempt to coalesce the
41
+ # exception object into the appropriate Gruf::Client::Errors subclass, or fallback to the default class if none
42
+ # is found (or it is a StandardError or higher-level error). It will leave alone Signals instead of attempting to
43
+ # coalesce them.
44
+ #
45
+ # @param [Exception] exception
46
+ # @return [Gruf::Client::Errors::Base|SignalException]
47
+ #
48
+ def from_exception(exception)
49
+ # passthrough on Signals, we don't want to mess with these
50
+ return exception if exception.is_a?(SignalException)
51
+
52
+ exception_class = determine_class(exception)
53
+ if exception.is_a?(GRPC::BadStatus)
54
+ # if it's a GRPC::BadStatus code, let's check for any trailing error metadata and decode it
55
+ exception_class.new(deserialize(exception))
56
+ else
57
+ # otherwise, let's just capture the error and build the wrapper class
58
+ exception_class.new(exception)
59
+ end
60
+ end
61
+
62
+ private
63
+
64
+ ##
65
+ # Deserialize any trailing metadata error payload from the exception
66
+ #
67
+ # @param [Gruf::Client::Errors::Base]
68
+ # @return [String]
69
+ #
70
+ def deserialize(exception)
71
+ if exception.respond_to?(:metadata)
72
+ key = exception.metadata.key?(@metadata_key.to_s) ? @metadata_key.to_s : @metadata_key.to_sym
73
+ return @deserializer_class.new(exception.metadata[key]).deserialize if exception.metadata.key?(key)
74
+ end
75
+
76
+ exception
77
+ end
78
+
79
+ ##
80
+ # @param [Exception] exception
81
+ # @return [Gruf::Client::Errors::Base]
82
+ #
83
+ def determine_class(exception)
84
+ error_class = Gruf::Client::Errors.const_get(exception.class.name.demodulize)
85
+
86
+ # Ruby module inheritance will have StandardError, ScriptError, etc still get to this point
87
+ # So we need to explicitly check for ancestry here
88
+ return @default_class unless error_class.ancestors.include?(Gruf::Client::Errors::Base)
89
+
90
+ error_class
91
+ rescue NameError => _e
92
+ @default_class
93
+ end
94
+
95
+ ##
96
+ # @return [Gruf::Serializers::Errors::Base]
97
+ #
98
+ def default_serializer
99
+ return Gruf::Serializers::Errors::Json unless Gruf.error_serializer
100
+
101
+ Gruf.error_serializer.is_a?(Class) ? Gruf.error_serializer : Gruf.error_serializer.to_s.constantize
102
+ end
103
+ end
104
+ end
105
+ end
data/lib/gruf/client.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # Copyright (c) 2017-present, BigCommerce Pty. Ltd. All rights reserved
2
4
  #
3
5
  # Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
@@ -13,6 +15,9 @@
13
15
  # COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
14
16
  # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
15
17
  #
18
+ require_relative 'client/error'
19
+ require_relative 'client/error_factory'
20
+
16
21
  module Gruf
17
22
  ##
18
23
  # Abstracts out the calling interface for interacting with gRPC clients. Streamlines calling and provides
@@ -33,25 +38,6 @@ module Gruf
33
38
  class Client < SimpleDelegator
34
39
  include Gruf::Loggable
35
40
 
36
- ##
37
- # Represents an error that was returned from the server's trailing metadata. Used as a custom exception object
38
- # that is instead raised in the case of the service returning serialized error data, as opposed to the normal
39
- # GRPC::BadStatus error
40
- #
41
- class Error < StandardError
42
- # @return [Object] error The deserialized error
43
- attr_reader :error
44
-
45
- ##
46
- # Initialize the client error
47
- #
48
- # @param [Object] error The deserialized error
49
- #
50
- def initialize(error)
51
- @error = error
52
- end
53
- end
54
-
55
41
  # @return [Class] The base, friendly name of the service being requested
56
42
  attr_reader :base_klass
57
43
  # @return [Class] The class name of the gRPC service being requested
@@ -72,9 +58,12 @@ module Gruf
72
58
  @base_klass = service
73
59
  @service_klass = "#{base_klass}::Service".constantize
74
60
  @opts = options || {}
75
- @opts[:password] = options.fetch(:password, '').to_s
76
- @opts[:hostname] = options.fetch(:hostname, Gruf.default_client_host)
77
- client = "#{service}::Stub".constantize.new(@opts[:hostname], build_ssl_credentials, client_options)
61
+ @opts[:password] = @opts.fetch(:password, '').to_s
62
+ @opts[:hostname] = @opts.fetch(:hostname, Gruf.default_client_host)
63
+ @opts[:channel_credentials] = @opts.fetch(:channel_credentials, Gruf.default_channel_credentials)
64
+ @error_factory = Gruf::Client::ErrorFactory.new
65
+ client_options[:timeout] = parse_timeout(client_options[:timeout]) if client_options.key?(:timeout)
66
+ client = "#{service}::Stub".constantize.new(@opts[:hostname], build_ssl_credentials, **client_options)
78
67
  super(client)
79
68
  end
80
69
 
@@ -82,8 +71,8 @@ module Gruf
82
71
  # Call the client's method with given params
83
72
  #
84
73
  # @param [String|Symbol] request_method The method that is being requested on the service
85
- # @param [Hash] params (Optional) A hash of parameters that will be inserted into the gRPC request message that is required
86
- # for the given above call
74
+ # @param [Hash] params (Optional) A hash of parameters that will be inserted into the gRPC request message that is
75
+ # required for the given above call
87
76
  # @param [Hash] metadata (Optional) A hash of metadata key/values that are transported with the client request
88
77
  # @param [Hash] opts (Optional) A hash of options to send to the gRPC request_response method
89
78
  # @return [Gruf::Response] The response from the server
@@ -96,18 +85,15 @@ module Gruf
96
85
  md = build_metadata(metadata)
97
86
  call_sig = call_signature(request_method)
98
87
 
99
- raise NotImplementedError, "The method #{request_method} has not been implemented in this service." unless call_sig
88
+ unless call_sig
89
+ raise NotImplementedError, "The method #{request_method} has not been implemented in this service."
90
+ end
91
+
92
+ resp, operation = execute(call_sig, req, md, opts, &block)
100
93
 
101
- resp = execute(call_sig, req, md, opts, &block)
94
+ raise @error_factory.from_exception(resp.result) unless resp.success?
102
95
 
103
- Gruf::Response.new(resp.result, resp.time)
104
- rescue GRPC::BadStatus => e
105
- emk = Gruf.error_metadata_key.to_s
106
- raise Gruf::Client::Error, error_deserializer_class.new(e.metadata[emk]).deserialize if e.respond_to?(:metadata) && e.metadata.key?(emk)
107
- raise # passthrough
108
- rescue StandardError => e
109
- Gruf.logger.error e.message
110
- raise
96
+ Gruf::Response.new(operation: operation, message: resp.result, execution_time: resp.time)
111
97
  end
112
98
 
113
99
  ##
@@ -137,13 +123,17 @@ module Gruf
137
123
  # @param [Object] req (Optional) The protobuf request message to send
138
124
  # @param [Hash] metadata (Optional) A hash of metadata key/values that are transported with the client request
139
125
  # @param [Hash] opts (Optional) A hash of options to send to the gRPC request_response method
126
+ # @return [Array<Gruf::Timer::Result, GRPC::ActiveCall::Operation>]
140
127
  #
141
128
  def execute(call_sig, req, metadata, opts = {}, &block)
142
- Timer.time do
129
+ operation = nil
130
+ result = Gruf::Timer.time do
143
131
  opts[:return_op] = true
144
132
  opts[:metadata] = metadata
145
- send(call_sig, req, opts, &block)
133
+ operation = send(call_sig, req, opts, &block)
134
+ operation.execute
146
135
  end
136
+ [result, operation]
147
137
  end
148
138
 
149
139
  ##
@@ -165,7 +155,7 @@ module Gruf
165
155
  #
166
156
  def request_object(request_method, params = {})
167
157
  desc = rpc_desc(request_method)
168
- desc && desc.input ? desc.input.new(params) : nil
158
+ desc&.input ? desc.input.new(params) : nil
169
159
  end
170
160
 
171
161
  ##
@@ -175,7 +165,7 @@ module Gruf
175
165
  #
176
166
  def call_signature(request_method)
177
167
  desc = rpc_desc(request_method)
178
- desc && desc.name ? desc.name.to_s.underscore.to_sym : nil
168
+ desc&.name ? desc.name.to_s.underscore.to_sym : nil
179
169
  end
180
170
 
181
171
  ##
@@ -201,6 +191,8 @@ module Gruf
201
191
  #
202
192
  # :nocov:
203
193
  def build_ssl_credentials
194
+ return opts[:channel_credentials] if opts[:channel_credentials]
195
+
204
196
  cert = nil
205
197
  if opts[:ssl_certificate_file]
206
198
  cert = File.read(opts[:ssl_certificate_file]).to_s.strip
@@ -224,5 +216,27 @@ module Gruf
224
216
  Gruf::Serializers::Errors::Json
225
217
  end
226
218
  end
219
+
220
+ ##
221
+ # Handle various timeout values and prevent improper value setting
222
+ #
223
+ # @see GRPC::Core::TimeConsts#from_relative_time
224
+ # @param [mixed] timeout
225
+ # @return [Float]
226
+ # @return [GRPC::Core::TimeSpec]
227
+ #
228
+ def parse_timeout(timeout)
229
+ if timeout.nil?
230
+ GRPC::Core::TimeConsts::ZERO
231
+ elsif timeout.is_a?(GRPC::Core::TimeSpec)
232
+ timeout
233
+ elsif timeout.is_a?(Numeric)
234
+ timeout
235
+ elsif timeout.respond_to?(:to_f)
236
+ timeout.to_f
237
+ else
238
+ raise ArgumentError, 'timeout is not a valid value: does not respond to to_f'
239
+ end
240
+ end
227
241
  end
228
242
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # Copyright (c) 2017-present, BigCommerce Pty. Ltd. All rights reserved
2
4
  #
3
5
  # Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
@@ -23,6 +25,8 @@ module Gruf
23
25
  server_binding_url: '0.0.0.0:9001',
24
26
  server_options: {},
25
27
  interceptors: nil,
28
+ hooks: nil,
29
+ default_channel_credentials: nil,
26
30
  default_client_host: '',
27
31
  use_ssl: false,
28
32
  ssl_crt_file: '',
@@ -36,17 +40,32 @@ module Gruf
36
40
  append_server_errors_to_trailing_metadata: true,
37
41
  use_default_interceptors: true,
38
42
  backtrace_on_error: false,
43
+ backtrace_limit: 10,
39
44
  use_exception_message: true,
40
- internal_error_message: 'Internal Server Error'
45
+ internal_error_message: 'Internal Server Error',
46
+ event_listener_proc: nil,
47
+ synchronized_client_internal_cache_expiry: 60,
48
+ rpc_server_options: {
49
+ pool_size: GRPC::RpcServer::DEFAULT_POOL_SIZE,
50
+ max_waiting_requests: GRPC::RpcServer::DEFAULT_MAX_WAITING_REQUESTS,
51
+ poll_period: GRPC::RpcServer::DEFAULT_POLL_PERIOD,
52
+ pool_keep_alive: GRPC::Pool::DEFAULT_KEEP_ALIVE,
53
+ connect_md_proc: nil,
54
+ server_args: {}
55
+ }.freeze
41
56
  }.freeze
42
57
 
43
- attr_accessor *VALID_CONFIG_KEYS.keys
58
+ attr_accessor(* VALID_CONFIG_KEYS.keys)
44
59
 
45
60
  ##
46
61
  # Whenever this is extended into a class, setup the defaults
47
62
  #
48
63
  def self.extended(base)
49
- base.reset
64
+ if defined?(Rails)
65
+ Gruf::Integrations::Rails::Railtie.config.before_initialize { base.reset }
66
+ else
67
+ base.reset
68
+ end
50
69
  end
51
70
 
52
71
  ##
@@ -79,15 +98,16 @@ module Gruf
79
98
  #
80
99
  def reset
81
100
  VALID_CONFIG_KEYS.each do |k, v|
82
- send((k.to_s + '='), v)
101
+ send("#{k}=", v)
83
102
  end
84
103
  self.interceptors = Gruf::Interceptors::Registry.new
104
+ self.hooks = Gruf::Hooks::Registry.new
85
105
  self.root_path = Rails.root.to_s.chomp('/') if defined?(Rails)
86
106
  if defined?(Rails) && Rails.logger
87
107
  self.logger = Rails.logger
88
108
  else
89
109
  require 'logger'
90
- self.logger = ::Logger.new(STDOUT)
110
+ self.logger = ::Logger.new($stdout)
91
111
  end
92
112
  self.grpc_logger = logger if grpc_logger.nil?
93
113
  self.ssl_crt_file = "#{root_path}config/ssl/#{environment}.crt"
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # Copyright (c) 2017-present, BigCommerce Pty. Ltd. All rights reserved
2
4
  #
3
5
  # Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
@@ -29,14 +31,19 @@ module Gruf
29
31
  # @var [Gruf::Error] error
30
32
  attr_reader :error
31
33
 
34
+ class << self
35
+ # @var [GRPC::GenericService] bound_service
36
+ attr_reader :bound_service
37
+ end
38
+
32
39
  ##
33
40
  # Initialize the controller within the given request context
34
41
  #
35
- # @param [Symbol] method_key
36
- # @param [GRPC::GenericService] service
37
- # @param [GRPC::RpcDesc] rpc_desc
38
- # @param [GRPC::ActiveCall] active_call
39
- # @param [Google::Protobuf::MessageExts] message
42
+ # @param [Symbol] method_key The gRPC method that this controller relates to
43
+ # @param [GRPC::GenericService] service The gRPC service stub for this controller
44
+ # @param [GRPC::RpcDesc] rpc_desc The RPC descriptor for this service method
45
+ # @param [GRPC::ActiveCall] active_call The gRPC ActiveCall object
46
+ # @param [Google::Protobuf::MessageExts] message The incoming protobuf request message
40
47
  #
41
48
  def initialize(method_key:, service:, rpc_desc:, active_call:, message:)
42
49
  @request = Request.new(
@@ -53,25 +60,41 @@ module Gruf
53
60
  ##
54
61
  # Bind the controller to the given service and add it to the service registry
55
62
  #
56
- # @param [GRPC::GenericService] service
63
+ # @param [GRPC::GenericService] service The name of the service to bind this controller to
57
64
  #
58
65
  def self.bind(service)
59
- Gruf.services << service.name.constantize
60
- ServiceBinder.new(service).bind!(self)
66
+ service_class = service.name.constantize
67
+ Gruf.services << service_class
68
+ # rubocop:disable ThreadSafety/InstanceVariableInClassMethod
69
+ @bound_service = service_class
70
+ # rubocop:enable ThreadSafety/InstanceVariableInClassMethod
71
+ ServiceBinder.new(service_class).bind!(self)
72
+ end
73
+
74
+ ##
75
+ # Call a method on this controller.
76
+ # Override this in a subclass to modify the behavior around processing a method
77
+ #
78
+ # @param [Symbol] method_key The name of the gRPC service method being called as a Symbol
79
+ # @param [block] &block The passed block for executing the method
80
+ #
81
+ def process_action(method_key, &block)
82
+ send(method_key, &block)
61
83
  end
62
84
 
63
85
  ##
64
86
  # Call a method on this controller
65
87
  #
66
- # @param [Symbol] method_key
88
+ # @param [Symbol] method_key The name of the gRPC service method being called as a Symbol
89
+ # @param [block] &block The passed block for executing the method
67
90
  #
68
91
  def call(method_key, &block)
69
92
  Interceptors::Context.new(@interceptors).intercept! do
70
- send(method_key, &block)
93
+ process_action(method_key, &block)
71
94
  end
72
95
  rescue GRPC::BadStatus
73
- raise # passthrough
74
- rescue StandardError => e
96
+ raise # passthrough, to be caught by Gruf::Interceptors::Timer
97
+ rescue GRPC::Core::CallError, StandardError => e # CallError is not a StandardError
75
98
  set_debug_info(e.message, e.backtrace) if Gruf.backtrace_on_error
76
99
  error_message = Gruf.use_exception_message ? e.message : Gruf.internal_error_message
77
100
  fail!(:internal, :unknown, error_message)