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
@@ -33,11 +35,16 @@ module Gruf
33
35
  delegate :metadata, to: :active_call
34
36
  delegate :messages, :client_streamer?, :server_streamer?, :bidi_streamer?, :request_response?, to: :type
35
37
 
38
+ ##
39
+ # Abstract representation of a gRPC request type
40
+ #
36
41
  class Type
37
42
  delegate :client_streamer?, :server_streamer?, :bidi_streamer?, :request_response?, to: :@rpc_desc
38
43
 
39
44
  ##
40
- # @param [GRPC::RpcDesc] rpc_desc
45
+ # Initialize a new request type object
46
+ #
47
+ # @param [GRPC::RpcDesc] rpc_desc The RPC descriptor for the request type
41
48
  #
42
49
  def initialize(rpc_desc)
43
50
  @rpc_desc = rpc_desc
@@ -45,11 +52,13 @@ module Gruf
45
52
  end
46
53
 
47
54
  ##
55
+ # Initialize an inbound controller request object
56
+ #
48
57
  # @param [Symbol] method_key The method symbol of the RPC method being executed
49
58
  # @param [Class] service The class of the service being executed against
50
59
  # @param [GRPC::RpcDesc] rpc_desc The RPC descriptor of the call
51
60
  # @param [GRPC::ActiveCall] active_call The restricted view of the call
52
- # @param [Object] message The protobuf message (or messages) of the request
61
+ # @param [Object|Google::Protobuf::MessageExts] message The protobuf message (or messages) of the request
53
62
  #
54
63
  def initialize(method_key:, service:, rpc_desc:, active_call:, message:)
55
64
  @method_key = method_key
@@ -64,21 +73,21 @@ module Gruf
64
73
  # Returns the service name as a translated name separated by periods. Strips
65
74
  # the superfluous "Service" suffix from the name
66
75
  #
67
- # @return [String]
76
+ # @return [String] The mapped service key
68
77
  #
69
78
  def service_key
70
79
  @service.name.underscore.tr('/', '.').gsub('.service', '')
71
80
  end
72
81
 
73
82
  ##
74
- # @return [Class]
83
+ # @return [Class] The class of the response message
75
84
  #
76
85
  def response_class
77
86
  @rpc_desc.output
78
87
  end
79
88
 
80
89
  ##
81
- # @return [Class]
90
+ # @return [Class] The class of the request message
82
91
  #
83
92
  def request_class
84
93
  @rpc_desc.input
@@ -90,21 +99,23 @@ module Gruf
90
99
  # @return [String] The parsed service method name
91
100
  #
92
101
  def method_name
93
- "#{service_key}.#{method_key}"
102
+ "#{service_key}.#{@method_key}"
94
103
  end
95
104
 
96
105
  ##
97
106
  # Return all messages for this request, properly handling different request types
98
107
  #
99
- # @return Enumerable<Object>
108
+ # @return [Enumerable<Object>] All messages for this request
100
109
  #
101
110
  def messages
102
111
  if client_streamer?
103
- message.call { |msg| yield msg }
112
+ # rubocop:disable Style/ExplicitBlockArgument
113
+ @message.call { |msg| yield msg }
114
+ # rubocop:enable Style/ExplicitBlockArgument
104
115
  elsif bidi_streamer?
105
- message
116
+ @message
106
117
  else
107
- [message]
118
+ [@message]
108
119
  end
109
120
  end
110
121
  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
@@ -25,14 +27,18 @@ module Gruf
25
27
  class BoundDesc < SimpleDelegator; end
26
28
 
27
29
  ##
28
- # @param [GRPC::GenericService] service
30
+ # Initialize a service binder instance with the given service
31
+ #
32
+ # @param [GRPC::GenericService] service The gRPC service stub to bind
29
33
  #
30
34
  def initialize(service)
31
35
  @service = service
32
36
  end
33
37
 
34
38
  ##
35
- # @param [Gruf::Controllers::Base]
39
+ # Bind all methods on the service to the passed controller
40
+ #
41
+ # @param [Class<Gruf::Controllers::Base>] controller
36
42
  #
37
43
  def bind!(controller)
38
44
  rpc_methods.each { |name, desc| bind_method(controller, name, desc) }
@@ -41,6 +47,8 @@ module Gruf
41
47
  private
42
48
 
43
49
  ##
50
+ # Bind the grpc methods to the service, allowing for server interception and execution control
51
+ #
44
52
  # @param [Gruf::Controllers::Base] controller
45
53
  # @param [Symbol] method_name
46
54
  # @param [BoundDesc] desc
@@ -52,22 +60,46 @@ module Gruf
52
60
  @service.class_eval do
53
61
  if desc.request_response?
54
62
  define_method(method_key) do |message, active_call|
55
- c = controller.new(method_key: method_key, service: service_ref, message: message, active_call: active_call, rpc_desc: desc)
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
+ )
56
70
  c.call(method_key)
57
71
  end
58
72
  elsif desc.client_streamer?
59
73
  define_method(method_key) do |active_call|
60
- c = controller.new(method_key: method_key, service: service_ref, message: proc { |&block| active_call.each_remote_read(&block) }, active_call: active_call, rpc_desc: desc)
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
+ )
61
81
  c.call(method_key)
62
82
  end
63
83
  elsif desc.server_streamer?
64
84
  define_method(method_key) do |message, active_call, &block|
65
- c = controller.new(method_key: method_key, service: service_ref, message: message, active_call: active_call, rpc_desc: desc)
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
+ )
66
92
  c.call(method_key, &block)
67
93
  end
68
94
  else # bidi
69
95
  define_method(method_key) do |messages, active_call, &block|
70
- c = controller.new(method_key: method_key, service: service_ref, message: messages, active_call: active_call, rpc_desc: desc)
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
+ )
71
103
  c.call(method_key, &block)
72
104
  end
73
105
  end
data/lib/gruf/error.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
@@ -50,6 +52,13 @@ module Gruf
50
52
  data_loss: GRPC::DataLoss
51
53
  }.freeze
52
54
 
55
+ # Default limit on trailing metadata is 8KB. We need to be careful
56
+ # not to overflow this limit, or the response message will never
57
+ # be sent. Instead, resource_exhausted will be thrown.
58
+ MAX_METADATA_SIZE = 7.5 * 1_024
59
+ METADATA_SIZE_EXCEEDED_CODE = 'metadata_size_exceeded'
60
+ METADATA_SIZE_EXCEEDED_MSG = 'Metadata too long, risks exceeding http2 trailing metadata limit.'
61
+
53
62
  # @return [Symbol] The given internal gRPC code for the error
54
63
  attr_accessor :code
55
64
  # @return [Symbol] An arbitrary application code that can be used for logical processing of the error by the client
@@ -58,8 +67,8 @@ module Gruf
58
67
  attr_accessor :message
59
68
  # @return [Array] An array of field errors that can be returned by the server
60
69
  attr_accessor :field_errors
61
- # @return [Object] A hash of debugging information, such as a stack trace and exception name, that can be used to
62
- # debug an given error response. This is sent by the server over the trailing metadata.
70
+ # @return [Errors::DebugInfo] A object containing debugging information, such as a stack trace and exception name,
71
+ # that can be used to debug an given error response. This is sent by the server over the trailing metadata.
63
72
  attr_accessor :debug_info
64
73
  # @return [GRPC::BadStatus] The gRPC BadStatus error object that was generated
65
74
  attr_writer :grpc_error
@@ -72,10 +81,11 @@ module Gruf
72
81
  # @param [Hash] args (Optional) An optional hash of arguments that will set fields on the error object
73
82
  #
74
83
  def initialize(args = {})
84
+ @field_errors = []
85
+ @metadata = {}
75
86
  args.each do |k, v|
76
87
  send("#{k}=", v) if respond_to?(k)
77
88
  end
78
- @field_errors = []
79
89
  end
80
90
 
81
91
  ##
@@ -116,7 +126,7 @@ module Gruf
116
126
  # @return [Hash] The newly set metadata
117
127
  #
118
128
  def metadata=(metadata)
119
- @metadata = metadata.map { |k, str| [k, str.to_s] }.to_h
129
+ @metadata = metadata.transform_values(&:to_s)
120
130
  end
121
131
 
122
132
  ##
@@ -130,16 +140,30 @@ module Gruf
130
140
  end
131
141
 
132
142
  ##
133
- # Append any appropriate errors to the gRPC call and properly update the output metadata
143
+ # Update the trailing metadata on the given gRPC call, including the error payload if configured
144
+ # to do so.
134
145
  #
135
146
  # @param [GRPC::ActiveCall] active_call The marshalled gRPC call
136
- # @return [Error] Return the error itself with the GRPC::ActiveCall attached and error metadata appended
147
+ # @return [Error] Return the error itself after updating metadata on the given gRPC call.
148
+ # In the case of a metadata overflow error, we replace the current error with
149
+ # a new one that won't cause a low-level http2 error.
137
150
  #
138
151
  def attach_to_call(active_call)
139
152
  metadata[Gruf.error_metadata_key.to_sym] = serialize if Gruf.append_server_errors_to_trailing_metadata
140
- if !metadata.empty? && active_call && active_call.respond_to?(:output_metadata)
141
- active_call.output_metadata.update(metadata)
153
+ return self if metadata.empty? || !active_call || !active_call.respond_to?(:output_metadata)
154
+
155
+ # Check if we've overflown the maximum size of output metadata. If so,
156
+ # log a warning and replace the metadata with something smaller to avoid
157
+ # resource exhausted errors.
158
+ if metadata.inspect.size > MAX_METADATA_SIZE
159
+ code = METADATA_SIZE_EXCEEDED_CODE
160
+ msg = METADATA_SIZE_EXCEEDED_MSG
161
+ logger.warn "#{code}: #{msg} Original error: #{to_h.inspect}"
162
+ err = Gruf::Error.new(code: :internal, app_code: code, message: msg)
163
+ return err.attach_to_call(active_call)
142
164
  end
165
+
166
+ active_call.output_metadata.update(metadata)
143
167
  self
144
168
  end
145
169
 
@@ -175,7 +199,8 @@ module Gruf
175
199
  # @return [GRPC::BadStatus]
176
200
  #
177
201
  def grpc_error
178
- @grpc_error = grpc_class.new(message, **@metadata)
202
+ md = @metadata || {}
203
+ @grpc_error = grpc_class.new(message, **md)
179
204
  end
180
205
 
181
206
  private
@@ -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
@@ -30,13 +32,18 @@ module Gruf
30
32
  #
31
33
  def initialize(detail, stack_trace = [])
32
34
  @detail = detail
33
- @stack_trace = stack_trace.is_a?(String) ? stack_trace.split("\n") : stack_trace
35
+ @stack_trace = (stack_trace.is_a?(String) ? stack_trace.split("\n") : stack_trace)
36
+
37
+ # Limit the size of the stack trace to reduce risk of overflowing metadata
38
+ stack_trace_limit = Gruf.backtrace_limit.to_i
39
+ stack_trace_limit = 10 if stack_trace_limit.negative?
40
+ @stack_trace = @stack_trace[0..stack_trace_limit] if stack_trace_limit.positive?
34
41
  end
35
42
 
36
43
  ##
37
44
  # Return this object marshalled into a hash
38
45
  #
39
- # @return [Hash] The debug info represented as a has
46
+ # @return [Hash] The debug info represented as a hash
40
47
  #
41
48
  def to_h
42
49
  {
@@ -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
@@ -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
@@ -15,6 +17,9 @@
15
17
  #
16
18
  module Gruf
17
19
  module Errors
20
+ ##
21
+ # Helper module for standardizing error interaction
22
+ #
18
23
  module Helpers
19
24
  delegate :add_field_error, :set_debug_info, :has_field_errors?, to: :error
20
25
 
@@ -0,0 +1,34 @@
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 Hooks
20
+ ##
21
+ # Base class for a hook that allows execution at various points of Gruf server processes
22
+ #
23
+ class Base
24
+ include Gruf::Loggable
25
+
26
+ ##
27
+ # @param [Hash] options
28
+ #
29
+ def initialize(options: nil)
30
+ @options = options || {}
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,47 @@
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 Hooks
20
+ ##
21
+ # Base class for a hook that allows execution at various points of gRPC server processes
22
+ #
23
+ class Executor
24
+ include Gruf::Loggable
25
+
26
+ def initialize(hooks: nil)
27
+ @hooks = hooks || Gruf.hooks&.prepare || []
28
+ end
29
+
30
+ ##
31
+ # Execute a hook point for each registered hook in the registry
32
+ #
33
+ # @param [Symbol] name
34
+ # @param [Hash] arguments
35
+ #
36
+ def call(name, arguments = {})
37
+ name = name.to_sym
38
+
39
+ @hooks.each do |hook|
40
+ next unless hook.respond_to?(name)
41
+
42
+ hook.send(name, **arguments)
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,159 @@
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 Hooks
20
+ ##
21
+ # Handles registration of hooks
22
+ #
23
+ class Registry
24
+ class HookNotFoundError < StandardError; end
25
+
26
+ def initialize
27
+ @registry = []
28
+ end
29
+
30
+ ##
31
+ # Add a hook to the registry
32
+ #
33
+ # @param [Class] hook_class The class of the hook to add
34
+ # @param [Hash] options A hash of options to pass into the hook during initialization
35
+ #
36
+ def use(hook_class, options = {})
37
+ hooks_mutex do
38
+ @registry << {
39
+ klass: hook_class,
40
+ options: options
41
+ }
42
+ end
43
+ end
44
+
45
+ ##
46
+ # Remove a hook from the registry
47
+ #
48
+ # @param [Class] hook_class The hook class to remove
49
+ # @raise [HookNotFoundError] if the hook is not found
50
+ #
51
+ def remove(hook_class)
52
+ pos = @registry.find_index { |opts| opts.fetch(:klass, '') == hook_class }
53
+ raise HookNotFoundError if pos.nil?
54
+
55
+ @registry.delete_at(pos)
56
+ end
57
+
58
+ ##
59
+ # Insert a hook before another specified hook
60
+ #
61
+ # @param [Class] before_class The hook to insert before
62
+ # @param [Class] hook_class The class of the hook to add
63
+ # @param [Hash] options A hash of options to pass into the hook during initialization
64
+ # @raise [HookNotFoundError] if the before hook is not found
65
+ #
66
+ def insert_before(before_class, hook_class, options = {})
67
+ hooks_mutex do
68
+ pos = @registry.find_index { |opts| opts.fetch(:klass, '') == before_class }
69
+ raise HookNotFoundError if pos.nil?
70
+
71
+ @registry.insert(
72
+ pos,
73
+ klass: hook_class,
74
+ options: options
75
+ )
76
+ end
77
+ end
78
+
79
+ ##
80
+ # Insert a hook after another specified hook
81
+ #
82
+ # @param [Class] after_class The hook to insert after
83
+ # @param [Class] hook_class The class of the hook to add
84
+ # @param [Hash] options A hash of options to pass into the hook during initialization
85
+ # @raise [HookNotFoundError] if the after hook is not found
86
+ #
87
+ def insert_after(after_class, hook_class, options = {})
88
+ hooks_mutex do
89
+ pos = @registry.find_index { |opts| opts.fetch(:klass, '') == after_class }
90
+ raise HookNotFoundError if pos.nil?
91
+
92
+ @registry.insert(
93
+ (pos + 1),
94
+ klass: hook_class,
95
+ options: options
96
+ )
97
+ end
98
+ end
99
+
100
+ ##
101
+ # Return a list of the hook classes in the registry in their execution order
102
+ #
103
+ # @return [Array<Class>]
104
+ #
105
+ def list
106
+ hooks_mutex do
107
+ @registry.map { |h| h[:klass] }
108
+ end
109
+ end
110
+
111
+ ##
112
+ # Lazily load and return all hooks for the given request
113
+ #
114
+ # @return [Array<Gruf::Hooks::Base>]
115
+ #
116
+ def prepare
117
+ is = []
118
+ hooks_mutex do
119
+ @registry.each do |o|
120
+ is << o[:klass].new(options: o[:options])
121
+ end
122
+ end
123
+ is
124
+ end
125
+
126
+ ##
127
+ # Clear the registry
128
+ #
129
+ def clear
130
+ hooks_mutex do
131
+ @registry = []
132
+ end
133
+ end
134
+
135
+ ##
136
+ # @return [Integer] The number of hooks currently loaded
137
+ #
138
+ def count
139
+ hooks_mutex do
140
+ @registry ||= []
141
+ @registry.count
142
+ end
143
+ end
144
+
145
+ private
146
+
147
+ ##
148
+ # Handle mutations to the hook registry in a thread-safe manner
149
+ #
150
+ def hooks_mutex(&block)
151
+ @hooks_mutex ||= begin
152
+ require 'monitor'
153
+ Monitor.new
154
+ end
155
+ @hooks_mutex.synchronize(&block)
156
+ end
157
+ end
158
+ end
159
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Gruf
4
+ ##
5
+ # A subclass of GRPC::RpcServer that can be used for enhanced monitoring
6
+ # of thread pool. Note that since we are reaching into the internals of
7
+ # GRPC::RpcServer, we need to watch the evolution of that class.
8
+ #
9
+ class InstrumentableGrpcServer < GRPC::RpcServer
10
+ ##
11
+ # Add an event_listener_proc that, if supplied, will be called
12
+ # when interesting events happen in the server.
13
+ #
14
+ def initialize(pool_size: DEFAULT_POOL_SIZE,
15
+ max_waiting_requests: DEFAULT_MAX_WAITING_REQUESTS,
16
+ poll_period: DEFAULT_POLL_PERIOD,
17
+ pool_keep_alive: Pool::DEFAULT_KEEP_ALIVE,
18
+ connect_md_proc: nil,
19
+ server_args: {},
20
+ interceptors: [],
21
+ event_listener_proc: nil)
22
+ # Call the base class initializer
23
+ super(
24
+ pool_size: pool_size,
25
+ max_waiting_requests: max_waiting_requests,
26
+ poll_period: poll_period,
27
+ pool_keep_alive: pool_keep_alive,
28
+ connect_md_proc: connect_md_proc,
29
+ server_args: server_args,
30
+ interceptors: interceptors
31
+ )
32
+
33
+ # Save event listener for later
34
+ @event_listener_proc = event_listener_proc
35
+ end
36
+
37
+ ##
38
+ # Notify the event listener of something interesting
39
+ #
40
+ def notify(event)
41
+ return if @event_listener_proc.nil? || !@event_listener_proc.respond_to?(:call)
42
+
43
+ @event_listener_proc.call(event)
44
+ end
45
+
46
+ ##
47
+ # Hook into the thread pool availability check for monitoring
48
+ #
49
+ def available?(an_rpc)
50
+ super.tap do |obj|
51
+ notify(:thread_pool_exhausted) unless obj
52
+ end
53
+ end
54
+
55
+ ##
56
+ # Hook into the method implementation check for monitoring
57
+ #
58
+ def implemented?(an_rpc)
59
+ super.tap do |obj|
60
+ notify(:unimplemented) unless obj
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Gruf
4
+ module Integrations
5
+ module Rails
6
+ class Railtie < ::Rails::Railtie
7
+ end
8
+ end
9
+ end
10
+ 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
@@ -25,9 +27,8 @@ module Gruf
25
27
  # connection pool, we need to ensure that this is done to properly
26
28
  #
27
29
  def call
28
- if enabled? && !::ActiveRecord::Base.connection.active?
29
- ::ActiveRecord::Base.establish_connection
30
- end
30
+ ::ActiveRecord::Base.establish_connection if enabled? && !::ActiveRecord::Base.connection.active?
31
+
31
32
  yield
32
33
  ensure
33
34
  ::ActiveRecord::Base.clear_active_connections! if enabled?