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
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]
|
36
|
+
# @param [Hash] opts
|
35
37
|
#
|
36
|
-
def initialize(
|
37
|
-
@options =
|
38
|
-
@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
|
-
|
56
|
-
|
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 {
|
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]
|
148
|
+
# @param [Hash] opts A hash of options for the interceptor
|
121
149
|
#
|
122
|
-
def insert_interceptor_before(before_class, interceptor_class,
|
150
|
+
def insert_interceptor_before(before_class, interceptor_class, opts = {})
|
123
151
|
raise ServerAlreadyStartedError if @started
|
124
|
-
|
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]
|
161
|
+
# @param [Hash] opts A hash of options for the interceptor
|
131
162
|
#
|
132
|
-
def insert_interceptor_after(after_class, interceptor_class,
|
163
|
+
def insert_interceptor_after(after_class, interceptor_class, opts = {})
|
133
164
|
raise ServerAlreadyStartedError if @started
|
134
|
-
|
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
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
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
|
-
|
32
|
-
|
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.
|
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
|