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.
- checksums.yaml +5 -5
- data/CHANGELOG.md +107 -12
- data/CODE_OF_CONDUCT.md +38 -41
- data/README.md +8 -360
- data/bin/gruf +2 -12
- data/gruf.gemspec +31 -7
- data/lib/gruf/cli/executor.rb +102 -0
- data/lib/gruf/client/error.rb +68 -0
- data/lib/gruf/client/error_factory.rb +105 -0
- data/lib/gruf/client.rb +52 -38
- data/lib/gruf/configuration.rb +25 -5
- data/lib/gruf/controllers/base.rb +35 -12
- data/lib/gruf/controllers/request.rb +21 -10
- data/lib/gruf/controllers/service_binder.rb +38 -6
- data/lib/gruf/error.rb +34 -9
- data/lib/gruf/errors/debug_info.rb +9 -2
- data/lib/gruf/errors/field.rb +2 -0
- data/lib/gruf/errors/helpers.rb +5 -0
- data/lib/gruf/hooks/base.rb +34 -0
- data/lib/gruf/hooks/executor.rb +47 -0
- data/lib/gruf/hooks/registry.rb +159 -0
- data/lib/gruf/instrumentable_grpc_server.rb +64 -0
- data/lib/gruf/integrations/rails/railtie.rb +10 -0
- data/lib/gruf/interceptors/active_record/connection_reset.rb +4 -3
- data/lib/gruf/interceptors/authentication/basic.rb +10 -2
- data/lib/gruf/interceptors/base.rb +3 -0
- data/lib/gruf/interceptors/client_interceptor.rb +117 -0
- data/lib/gruf/interceptors/context.rb +6 -4
- data/lib/gruf/interceptors/instrumentation/output_metadata_timer.rb +5 -4
- data/lib/gruf/interceptors/instrumentation/request_logging/formatters/base.rb +5 -1
- data/lib/gruf/interceptors/instrumentation/request_logging/formatters/logstash.rb +5 -1
- data/lib/gruf/interceptors/instrumentation/request_logging/formatters/plain.rb +5 -1
- data/lib/gruf/interceptors/instrumentation/request_logging/interceptor.rb +60 -29
- data/lib/gruf/interceptors/instrumentation/statsd.rb +5 -4
- data/lib/gruf/interceptors/registry.rb +6 -1
- data/lib/gruf/interceptors/server_interceptor.rb +2 -0
- data/lib/gruf/interceptors/timer.rb +12 -2
- data/lib/gruf/loggable.rb +2 -0
- data/lib/gruf/logging.rb +2 -0
- data/lib/gruf/outbound/request_context.rb +71 -0
- data/lib/gruf/response.rb +5 -2
- data/lib/gruf/serializers/errors/base.rb +2 -0
- data/lib/gruf/serializers/errors/json.rb +2 -0
- data/lib/gruf/server.rb +57 -23
- data/lib/gruf/synchronized_client.rb +97 -0
- data/lib/gruf/timer.rb +9 -6
- data/lib/gruf/version.rb +3 -1
- data/lib/gruf.rb +10 -0
- 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
|
-
#
|
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
|
-
|
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
|
-
#
|
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
|
-
#
|
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(
|
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(
|
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(
|
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(
|
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 [
|
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.
|
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
|
-
#
|
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
|
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
|
141
|
-
|
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
|
-
|
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
|
46
|
+
# @return [Hash] The debug info represented as a hash
|
40
47
|
#
|
41
48
|
def to_h
|
42
49
|
{
|
data/lib/gruf/errors/field.rb
CHANGED
data/lib/gruf/errors/helpers.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
|
@@ -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
|
@@ -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
|
-
|
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?
|