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
@@ -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
data/lib/gruf/server.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
@@ -31,11 +33,11 @@ module Gruf
31
33
  ##
32
34
  # Initialize the server and load and setup the services
33
35
  #
34
- # @param [Hash] options
36
+ # @param [Hash] opts
35
37
  #
36
- def initialize(options = {})
37
- @options = options || {}
38
- @interceptors = options.fetch(:interceptor_registry, Gruf.interceptors)
38
+ def initialize(opts = {})
39
+ @options = opts || {}
40
+ @interceptors = opts.fetch(:interceptor_registry, Gruf.interceptors)
39
41
  @interceptors = Gruf::Interceptors::Registry.new unless @interceptors.is_a?(Gruf::Interceptors::Registry)
40
42
  @services = []
41
43
  @started = false
@@ -43,6 +45,8 @@ module Gruf
43
45
  @stop_server_cv = ConditionVariable.new
44
46
  @stop_server_mu = Monitor.new
45
47
  @server_mu = Monitor.new
48
+ @hostname = opts.fetch(:hostname, Gruf.server_binding_url)
49
+ @event_listener_proc = opts.fetch(:event_listener_proc, Gruf.event_listener_proc)
46
50
  setup
47
51
  end
48
52
 
@@ -52,8 +56,25 @@ module Gruf
52
56
  def server
53
57
  @server_mu.synchronize do
54
58
  @server ||= begin
55
- server = GRPC::RpcServer.new(options)
56
- @port = server.add_http2_port(options.fetch(:hostname, Gruf.server_binding_url), ssl_credentials)
59
+ # For backward compatibility, we allow these options to be passed directly
60
+ # in the Gruf::Server options, or via Gruf.rpc_server_options.
61
+ server_options = {
62
+ pool_size: options.fetch(:pool_size, Gruf.rpc_server_options[:pool_size]),
63
+ max_waiting_requests: options.fetch(:max_waiting_requests, Gruf.rpc_server_options[:max_waiting_requests]),
64
+ poll_period: options.fetch(:poll_period, Gruf.rpc_server_options[:poll_period]),
65
+ pool_keep_alive: options.fetch(:pool_keep_alive, Gruf.rpc_server_options[:pool_keep_alive]),
66
+ connect_md_proc: options.fetch(:connect_md_proc, Gruf.rpc_server_options[:connect_md_proc]),
67
+ server_args: options.fetch(:server_args, Gruf.rpc_server_options[:server_args])
68
+ }
69
+
70
+ server = if @event_listener_proc
71
+ server_options[:event_listener_proc] = @event_listener_proc
72
+ Gruf::InstrumentableGrpcServer.new(**server_options)
73
+ else
74
+ GRPC::RpcServer.new(**server_options)
75
+ end
76
+
77
+ @port = server.add_http2_port(@hostname, ssl_credentials)
57
78
  @services.each { |s| server.handle(s) }
58
79
  server
59
80
  end
@@ -68,13 +89,14 @@ module Gruf
68
89
  update_proc_title(:starting)
69
90
 
70
91
  server_thread = Thread.new do
71
- logger.info { 'Booting gRPC Server...' }
92
+ logger.info { "Starting gruf server at #{@hostname}..." }
72
93
  server.run
73
94
  end
74
95
 
75
96
  stop_server_thread = Thread.new do
76
97
  loop do
77
98
  break if @stop_server
99
+
78
100
  @stop_server_mu.synchronize { @stop_server_cv.wait(@stop_server_mu, 10) }
79
101
  end
80
102
  logger.info { 'Shutting down...' }
@@ -94,11 +116,14 @@ module Gruf
94
116
  # :nocov:
95
117
 
96
118
  ##
119
+ # Add a gRPC service stub to be served by gruf
120
+ #
97
121
  # @param [Class] klass
98
122
  # @raise [ServerAlreadyStartedError] if the server is already started
99
123
  #
100
124
  def add_service(klass)
101
125
  raise ServerAlreadyStartedError if @started
126
+
102
127
  @services << klass unless @services.include?(klass)
103
128
  end
104
129
 
@@ -111,27 +136,34 @@ module Gruf
111
136
  #
112
137
  def add_interceptor(klass, opts = {})
113
138
  raise ServerAlreadyStartedError if @started
139
+
114
140
  @interceptors.use(klass, opts)
115
141
  end
116
142
 
117
143
  ##
144
+ # Insert an interceptor before another in the currently registered order of execution
145
+ #
118
146
  # @param [Class] before_class The interceptor that you want to add the new interceptor before
119
147
  # @param [Class] interceptor_class The Interceptor to add to the registry
120
- # @param [Hash] options A hash of options for the interceptor
148
+ # @param [Hash] opts A hash of options for the interceptor
121
149
  #
122
- def insert_interceptor_before(before_class, interceptor_class, options = {})
150
+ def insert_interceptor_before(before_class, interceptor_class, opts = {})
123
151
  raise ServerAlreadyStartedError if @started
124
- @interceptors.insert_before(before_class, interceptor_class, options)
152
+
153
+ @interceptors.insert_before(before_class, interceptor_class, opts)
125
154
  end
126
155
 
127
156
  ##
157
+ # Insert an interceptor after another in the currently registered order of execution
158
+ #
128
159
  # @param [Class] after_class The interceptor that you want to add the new interceptor after
129
160
  # @param [Class] interceptor_class The Interceptor to add to the registry
130
- # @param [Hash] options A hash of options for the interceptor
161
+ # @param [Hash] opts A hash of options for the interceptor
131
162
  #
132
- def insert_interceptor_after(after_class, interceptor_class, options = {})
163
+ def insert_interceptor_after(after_class, interceptor_class, opts = {})
133
164
  raise ServerAlreadyStartedError if @started
134
- @interceptors.insert_after(after_class, interceptor_class, options)
165
+
166
+ @interceptors.insert_after(after_class, interceptor_class, opts)
135
167
  end
136
168
 
137
169
  ##
@@ -146,8 +178,11 @@ module Gruf
146
178
  ##
147
179
  # Remove an interceptor from the server
148
180
  #
181
+ # @param [Class] klass
182
+ #
149
183
  def remove_interceptor(klass)
150
184
  raise ServerAlreadyStartedError if @started
185
+
151
186
  @interceptors.remove(klass)
152
187
  end
153
188
 
@@ -156,6 +191,7 @@ module Gruf
156
191
  #
157
192
  def clear_interceptors
158
193
  raise ServerAlreadyStartedError if @started
194
+
159
195
  @interceptors.clear
160
196
  end
161
197
 
@@ -176,8 +212,6 @@ module Gruf
176
212
  #
177
213
  # :nocov:
178
214
  def setup_signal_handlers
179
- Thread.abort_on_exception = true
180
-
181
215
  Signal.trap('INT') do
182
216
  @stop_server = true
183
217
  @stop_server_cv.broadcast
@@ -196,10 +230,12 @@ module Gruf
196
230
  # :nocov:
197
231
  def load_controllers
198
232
  return unless File.directory?(controllers_path)
233
+
199
234
  path = File.realpath(controllers_path)
200
235
  $LOAD_PATH.unshift(path)
201
236
  Dir["#{path}/**/*.rb"].each do |f|
202
237
  next if f.include?('_pb') # exclude if people include proto generated files in app/rpc
238
+
203
239
  logger.info "- Loading gRPC service file: #{f}"
204
240
  load File.realpath(f)
205
241
  end
@@ -220,14 +256,12 @@ module Gruf
220
256
  #
221
257
  # :nocov:
222
258
  def ssl_credentials
223
- if options.fetch(:use_ssl, Gruf.use_ssl)
224
- private_key = File.read(options.fetch(:ssl_key_file, Gruf.ssl_key_file))
225
- cert_chain = File.read(options.fetch(:ssl_crt_file, Gruf.ssl_crt_file))
226
- certs = [nil, [{ private_key: private_key, cert_chain: cert_chain }], false]
227
- GRPC::Core::ServerCredentials.new(*certs)
228
- else
229
- :this_port_is_insecure
230
- end
259
+ return :this_port_is_insecure unless options.fetch(:use_ssl, Gruf.use_ssl)
260
+
261
+ private_key = File.read(options.fetch(:ssl_key_file, Gruf.ssl_key_file))
262
+ cert_chain = File.read(options.fetch(:ssl_crt_file, Gruf.ssl_crt_file))
263
+ certs = [nil, [{ private_key: private_key, cert_chain: cert_chain }], false]
264
+ GRPC::Core::ServerCredentials.new(*certs)
231
265
  end
232
266
  # :nocov:
233
267
 
@@ -0,0 +1,97 @@
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 'concurrent'
19
+
20
+ module Gruf
21
+ ##
22
+ # Ensures that we only have one active call to a given endpoint with a given set of params. This can be useful
23
+ # to mitigate thundering herds.
24
+ #
25
+ class SynchronizedClient < Gruf::Client
26
+ attr_reader :unsynchronized_methods
27
+
28
+ ##
29
+ # Initialize the client and setup the stub
30
+ #
31
+ # @param [Module] service The namespace of the client Stub that is desired to load
32
+ # @param [Hash] options A hash of options for the client
33
+ # @option options [Array] :unsynchronized_methods A list of methods (as symbols) that
34
+ # should be excluded from synchronization
35
+ # @option options [Integer] :internal_cache_expiry The length of time to keep results
36
+ # around for other threads to fetch (in seconds)
37
+ # @param [Hash] client_options A hash of options to pass to the gRPC client stub
38
+ #
39
+ def initialize(service:, options: {}, client_options: {})
40
+ @unsynchronized_methods = options.delete(:unsynchronized_methods) { [] }
41
+ @expiry = options.delete(:internal_cache_expiry) { Gruf.synchronized_client_internal_cache_expiry }
42
+ @locks = Concurrent::Map.new
43
+ @results = Concurrent::Map.new
44
+ super
45
+ end
46
+
47
+ ##
48
+ # Call the client's method with given params. If another call is already active for the same endpoint and the same
49
+ # params, block until the active call is complete. When unblocked, callers will get a copy of the original result.
50
+ #
51
+ # @param [String|Symbol] request_method The method that is being requested on the service
52
+ # @param [Hash] params (Optional) A hash of parameters that will be inserted into the gRPC request
53
+ # message that is required for the given above call
54
+ # @param [Hash] metadata (Optional) A hash of metadata key/values that are transported with the client request
55
+ # @param [Hash] opts (Optional) A hash of options to send to the gRPC request_response method
56
+ # @return [Gruf::Response] The response from the server
57
+ # @raise [Gruf::Client::Error|GRPC::BadStatus] If an error occurs, an exception will be raised according to the
58
+ # error type that was returned
59
+ #
60
+ def call(request_method, params = {}, metadata = {}, opts = {}, &block)
61
+ # Allow for bypassing extra behavior for selected methods
62
+ return super if unsynchronized_methods.include?(request_method.to_sym)
63
+
64
+ # Generate a unique key based on the method and params
65
+ key = "#{request_method}.#{params.hash}"
66
+
67
+ # Create a lock for this call if we haven't seen it already, then acquire it
68
+ lock = @locks.compute_if_absent(key) { Mutex.new }
69
+ lock.synchronize do
70
+ # Return value from results cache if it exists. This occurs for callers that were
71
+ # waiting on the lock while the first caller was making the actual grpc call.
72
+ response = @results.get(lock)
73
+ if response
74
+ Gruf.logger.debug "Returning cached result for #{key}:#{lock.inspect}"
75
+ next response
76
+ end
77
+
78
+ # Make the grpc call and record response for other callers that are blocked
79
+ # on the same lock
80
+ response = super
81
+ @results.put(lock, response)
82
+
83
+ # Schedule a task to come in later and clean out result to prevent memory bloat
84
+ Concurrent::ScheduledTask.new(@expiry, args: [@results, lock]) { |h, k| h.delete(k) }.execute
85
+
86
+ # Remove the lock from the map. The next caller to come through with the
87
+ # same params will create a new lock and start the process over again.
88
+ # Anyone who was waiting on this call will be using a local reference
89
+ # to the same lock as us, and will fetch the result from the cache.
90
+ @locks.delete(key)
91
+
92
+ # Return response
93
+ response
94
+ end
95
+ end
96
+ end
97
+ end
data/lib/gruf/timer.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
@@ -27,11 +29,12 @@ module Gruf
27
29
  # result.time # => 1.10123
28
30
  # result.result # => 'my_thing_is_done'
29
31
  #
32
+ # @property [Object] result The result of the block that was called
33
+ # @property [Float] time The time, in ms, of the block execution
34
+ #
30
35
  class Result
31
- # @return [Object] result The result of the block that was called
32
- attr_reader :result
33
- # @return [Float] time The time, in ms, of the block execution
34
- attr_reader :time
36
+ attr_reader :result,
37
+ :time
35
38
 
36
39
  ##
37
40
  # Initialize the result object
@@ -50,7 +53,7 @@ module Gruf
50
53
  # @return [Boolean] Whether or not this result was a success
51
54
  #
52
55
  def success?
53
- !result.is_a?(GRPC::BadStatus)
56
+ !result.is_a?(GRPC::BadStatus) && !result.is_a?(StandardError) && !result.is_a?(GRPC::Core::CallError)
54
57
  end
55
58
  end
56
59
 
@@ -65,7 +68,7 @@ module Gruf
65
68
  start_time = Time.now
66
69
  begin
67
70
  result = yield
68
- rescue GRPC::BadStatus => e
71
+ rescue GRPC::BadStatus, StandardError, GRPC::Core::CallError => e
69
72
  result = e
70
73
  end
71
74
  end_time = Time.now
data/lib/gruf/version.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
@@ -14,5 +16,5 @@
14
16
  # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
15
17
  #
16
18
  module Gruf
17
- VERSION = '2.2.2'.freeze
19
+ VERSION = '2.9.1'
18
20
  end
data/lib/gruf.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
@@ -23,14 +25,22 @@ require_relative 'gruf/logging'
23
25
  require_relative 'gruf/loggable'
24
26
  require_relative 'gruf/configuration'
25
27
  require_relative 'gruf/errors/helpers'
28
+ require_relative 'gruf/cli/executor'
26
29
  require_relative 'gruf/controllers/base'
30
+ require_relative 'gruf/outbound/request_context'
27
31
  require_relative 'gruf/interceptors/registry'
28
32
  require_relative 'gruf/interceptors/base'
33
+ require_relative 'gruf/hooks/registry'
34
+ require_relative 'gruf/hooks/executor'
35
+ require_relative 'gruf/hooks/base'
29
36
  require_relative 'gruf/timer'
30
37
  require_relative 'gruf/response'
31
38
  require_relative 'gruf/error'
32
39
  require_relative 'gruf/client'
40
+ require_relative 'gruf/synchronized_client'
41
+ require_relative 'gruf/instrumentable_grpc_server'
33
42
  require_relative 'gruf/server'
43
+ require_relative 'gruf/integrations/rails/railtie' if defined?(Rails)
34
44
 
35
45
  ##
36
46
  # Initializes configuration of gruf core module