gruf 2.4.2 → 2.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +8 -0
- data/README.md +39 -3
- data/gruf.gemspec +1 -0
- data/lib/gruf.rb +1 -0
- data/lib/gruf/client.rb +14 -30
- data/lib/gruf/client/error.rb +64 -0
- data/lib/gruf/client/error_factory.rb +99 -0
- data/lib/gruf/configuration.rb +1 -0
- data/lib/gruf/interceptors/instrumentation/request_logging/interceptor.rb +2 -1
- data/lib/gruf/response.rb +3 -2
- data/lib/gruf/synchronized_client.rb +93 -0
- data/lib/gruf/timer.rb +2 -2
- data/lib/gruf/version.rb +1 -1
- metadata +19 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a2452f173e2987612fde71b5e170874f52718e5a7f820658378c7a98e54f5b1f
|
4
|
+
data.tar.gz: bcbb20ab199f78fadd1926d1314fe881fccda9413f149ebb8ad1e4daeba259f8
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d9ae08c458006784a089c1390000870febe48a01e8e6be85673e343639ec8283410c76fa6110981916f1333bc3e49c499d845cb03551cf1ea42cab8b5455c2c1
|
7
|
+
data.tar.gz: 26833397af9630db7a6c3d0429ddcaaf63d84e076e8d6691719b2210b8f7523b3a89169e2826ad2f66d67dd64e313a31f87da423d5f4580c4b7ac6fc26fc54d7
|
data/CHANGELOG.md
CHANGED
@@ -2,6 +2,14 @@ Changelog for the gruf gem. This includes internal history before the gem was ma
|
|
2
2
|
|
3
3
|
### Pending release
|
4
4
|
|
5
|
+
### 2.5.0
|
6
|
+
|
7
|
+
- Client exceptions raised now contain mapped subclasses, such as `Gruf::Client::Errors::InvalidArgument`
|
8
|
+
- Client exceptions will also now catch StandardError and GRPC::Core errors, and handle them as Internal errors
|
9
|
+
- Added SynchronizedClient which prevents multiple calls to the same endpoint with the same params at
|
10
|
+
a given time. This is useful for mitigating thundering herds. To skip this behavior for certain endpoints,
|
11
|
+
pass the `options[:unsynchronized_methods]` param with a list of method names (as symbols).
|
12
|
+
|
5
13
|
### 2.4.2
|
6
14
|
|
7
15
|
- Added error handling for GRPC::Core::CallError, a low-level error in the grpc library that does not inherit
|
data/README.md
CHANGED
@@ -48,7 +48,7 @@ Gruf.configure do |c|
|
|
48
48
|
end
|
49
49
|
```
|
50
50
|
|
51
|
-
If you don't explicitly set `default_client_host`, you will need to pass it into the
|
51
|
+
If you don't explicitly set `default_client_host`, you will need to pass it into the options, like so:
|
52
52
|
|
53
53
|
```ruby
|
54
54
|
client = ::Gruf::Client.new(service: ::Demo::ThingService, options: {hostname: 'grpc.service.com:9003'})
|
@@ -72,10 +72,46 @@ end
|
|
72
72
|
|
73
73
|
Note this returns a response object. The response object can provide `trailing_metadata` as well as a `execution_time`.
|
74
74
|
|
75
|
+
### SynchronizedClient
|
76
|
+
|
77
|
+
SynchronizedClient wraps Client with some additional behavior to help prevent generating spikes
|
78
|
+
of redundant requests. If multiple calls to the same endpoint with the same parameters are made,
|
79
|
+
the first one will be executed and the following ones will block, waiting for the first result.
|
80
|
+
|
81
|
+
```ruby
|
82
|
+
require 'gruf'
|
83
|
+
require 'thwait'
|
84
|
+
|
85
|
+
id = args[:id].to_i.presence || 1
|
86
|
+
client = ::Gruf::SynchronizedClient.new(service: ::Demo::ThingService)
|
87
|
+
thread1 = Thread.new { client.call(:GetMyThing, id: id) }
|
88
|
+
thread2 = Thread.new { client.call(:GetMyThing, id: id) }
|
89
|
+
ThreadsWait.all_waits(thread1, thread2)
|
90
|
+
```
|
91
|
+
|
92
|
+
In the above example, thread1 will make the rpc call, thread2 will block until the call is complete, and then
|
93
|
+
will get the same value without making a second rpc call.
|
94
|
+
|
95
|
+
You can also skip this behavior for certain methods if desired.
|
96
|
+
|
97
|
+
```ruby
|
98
|
+
require 'gruf'
|
99
|
+
require 'thwait'
|
100
|
+
|
101
|
+
id = args[:id].to_i.presence || 1
|
102
|
+
client = ::Gruf::SynchronizedClient.new(service: ::Demo::ThingService, options: { unsynchronized_methods: [:GetMyThing] })
|
103
|
+
thread1 = Thread.new { client.call(:GetMyThing, id: id) }
|
104
|
+
thread2 = Thread.new { client.call(:GetMyThing, id: id) }
|
105
|
+
ThreadsWait.all_waits(thread1, thread2)
|
106
|
+
```
|
107
|
+
|
108
|
+
In the above example, thread1 and thread2 will make rpc calls in parallel, in the same way as if you had used
|
109
|
+
`Gruf::Client`.
|
110
|
+
|
75
111
|
### Client Interceptors
|
76
112
|
|
77
113
|
Gruf comes with an assistance class for client interceptors that you can use - or you can use the native gRPC core
|
78
|
-
interceptors. Either way, you pass them into the client_options when creating a client:
|
114
|
+
interceptors. Either way, you pass them into the `client_options` when creating a client:
|
79
115
|
|
80
116
|
```ruby
|
81
117
|
class MyInterceptor < Gruf::Interceptors::ClientInterceptor
|
@@ -487,7 +523,7 @@ gruf that you can use today:
|
|
487
523
|
|
488
524
|
* [gruf-zipkin](https://github.com/bigcommerce/gruf-zipkin) - Provides a [Zipkin](https://zipkin.io)
|
489
525
|
integration
|
490
|
-
* [gruf-lightstep](https://github.com/bigcommerce/gruf-lightstep) - Provides a seamless
|
526
|
+
* [gruf-lightstep](https://github.com/bigcommerce/gruf-lightstep) - Provides a seamless
|
491
527
|
[LightStep](https://lightstep.com) integration
|
492
528
|
* [gruf-circuit-breaker](https://github.com/bigcommerce/gruf-circuit-breaker) - Circuit breaker
|
493
529
|
support for services
|
data/gruf.gemspec
CHANGED
@@ -39,5 +39,6 @@ Gem::Specification.new do |spec|
|
|
39
39
|
spec.add_runtime_dependency 'grpc', '~> 1.10'
|
40
40
|
spec.add_runtime_dependency 'grpc-tools', '~> 1.10'
|
41
41
|
spec.add_runtime_dependency 'activesupport'
|
42
|
+
spec.add_runtime_dependency 'concurrent-ruby'
|
42
43
|
spec.add_runtime_dependency 'slop', '~> 4.6'
|
43
44
|
end
|
data/lib/gruf.rb
CHANGED
@@ -32,6 +32,7 @@ require_relative 'gruf/timer'
|
|
32
32
|
require_relative 'gruf/response'
|
33
33
|
require_relative 'gruf/error'
|
34
34
|
require_relative 'gruf/client'
|
35
|
+
require_relative 'gruf/synchronized_client'
|
35
36
|
require_relative 'gruf/instrumentable_grpc_server'
|
36
37
|
require_relative 'gruf/server'
|
37
38
|
|
data/lib/gruf/client.rb
CHANGED
@@ -13,6 +13,9 @@
|
|
13
13
|
# COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
14
14
|
# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
15
15
|
#
|
16
|
+
require_relative 'client/error'
|
17
|
+
require_relative 'client/error_factory'
|
18
|
+
|
16
19
|
module Gruf
|
17
20
|
##
|
18
21
|
# Abstracts out the calling interface for interacting with gRPC clients. Streamlines calling and provides
|
@@ -33,25 +36,6 @@ module Gruf
|
|
33
36
|
class Client < SimpleDelegator
|
34
37
|
include Gruf::Loggable
|
35
38
|
|
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
39
|
# @return [Class] The base, friendly name of the service being requested
|
56
40
|
attr_reader :base_klass
|
57
41
|
# @return [Class] The class name of the gRPC service being requested
|
@@ -74,6 +58,7 @@ module Gruf
|
|
74
58
|
@opts = options || {}
|
75
59
|
@opts[:password] = options.fetch(:password, '').to_s
|
76
60
|
@opts[:hostname] = options.fetch(:hostname, Gruf.default_client_host)
|
61
|
+
@error_factory = Gruf::Client::ErrorFactory.new
|
77
62
|
client = "#{service}::Stub".constantize.new(@opts[:hostname], build_ssl_credentials, client_options)
|
78
63
|
super(client)
|
79
64
|
end
|
@@ -98,16 +83,11 @@ module Gruf
|
|
98
83
|
|
99
84
|
raise NotImplementedError, "The method #{request_method} has not been implemented in this service." unless call_sig
|
100
85
|
|
101
|
-
resp = execute(call_sig, req, md, opts, &block)
|
86
|
+
resp, operation = execute(call_sig, req, md, opts, &block)
|
87
|
+
|
88
|
+
raise @error_factory.from_exception(resp.result) unless resp.success?
|
102
89
|
|
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
|
90
|
+
Gruf::Response.new(operation: operation, message: resp.result, execution_time: resp.time)
|
111
91
|
end
|
112
92
|
|
113
93
|
##
|
@@ -137,13 +117,17 @@ module Gruf
|
|
137
117
|
# @param [Object] req (Optional) The protobuf request message to send
|
138
118
|
# @param [Hash] metadata (Optional) A hash of metadata key/values that are transported with the client request
|
139
119
|
# @param [Hash] opts (Optional) A hash of options to send to the gRPC request_response method
|
120
|
+
# @return [Array<Gruf::Timer::Result, GRPC::ActiveCall::Operation>]
|
140
121
|
#
|
141
122
|
def execute(call_sig, req, metadata, opts = {}, &block)
|
142
|
-
|
123
|
+
operation = nil
|
124
|
+
result = Gruf::Timer.time do
|
143
125
|
opts[:return_op] = true
|
144
126
|
opts[:metadata] = metadata
|
145
|
-
send(call_sig, req, opts, &block)
|
127
|
+
operation = send(call_sig, req, opts, &block)
|
128
|
+
operation.execute
|
146
129
|
end
|
130
|
+
[result, operation]
|
147
131
|
end
|
148
132
|
|
149
133
|
##
|
@@ -0,0 +1,64 @@
|
|
1
|
+
# Copyright (c) 2017-present, BigCommerce Pty. Ltd. All rights reserved
|
2
|
+
#
|
3
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
|
4
|
+
# documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
|
5
|
+
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit
|
6
|
+
# persons to whom the Software is furnished to do so, subject to the following conditions:
|
7
|
+
#
|
8
|
+
# The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
|
9
|
+
# Software.
|
10
|
+
#
|
11
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
|
12
|
+
# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
13
|
+
# COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
14
|
+
# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
15
|
+
#
|
16
|
+
module Gruf
|
17
|
+
class Client < SimpleDelegator
|
18
|
+
##
|
19
|
+
# Represents an error that was returned from the server's trailing metadata. Used as a custom exception object
|
20
|
+
# that is instead raised in the case of the service returning serialized error data, as opposed to the normal
|
21
|
+
# GRPC::BadStatus error
|
22
|
+
#
|
23
|
+
class Error < StandardError
|
24
|
+
# @return [Object] error The deserialized error
|
25
|
+
attr_reader :error
|
26
|
+
|
27
|
+
##
|
28
|
+
# Initialize the client error
|
29
|
+
#
|
30
|
+
# @param [Object] error The deserialized error
|
31
|
+
#
|
32
|
+
def initialize(error)
|
33
|
+
@error = error
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
##
|
38
|
+
# See https://github.com/grpc/grpc-go/blob/master/codes/codes.go for a detailed summary of each error type
|
39
|
+
#
|
40
|
+
module Errors
|
41
|
+
class Base < Gruf::Client::Error; end
|
42
|
+
class Error < Base; end
|
43
|
+
class Validation < Base; end
|
44
|
+
|
45
|
+
class Ok < Base; end
|
46
|
+
|
47
|
+
class InvalidArgument < Validation; end
|
48
|
+
class NotFound < Validation; end
|
49
|
+
class AlreadyExists < Validation; end
|
50
|
+
class OutOfRange < Validation; end
|
51
|
+
|
52
|
+
class Cancelled < Error; end
|
53
|
+
class DataLoss < Error; end
|
54
|
+
class DeadlineExceeded < Error; end
|
55
|
+
class FailedPrecondition < Error; end
|
56
|
+
class Internal < Error; end
|
57
|
+
class PermissionDenied < Error; end
|
58
|
+
class Unauthenticated < Error; end
|
59
|
+
class Unavailable < Error; end
|
60
|
+
class Unimplemented < Error; end
|
61
|
+
class Unknown < Error; end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
# Copyright (c) 2017-present, BigCommerce Pty. Ltd. All rights reserved
|
2
|
+
#
|
3
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
|
4
|
+
# documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
|
5
|
+
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit
|
6
|
+
# persons to whom the Software is furnished to do so, subject to the following conditions:
|
7
|
+
#
|
8
|
+
# The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
|
9
|
+
# Software.
|
10
|
+
#
|
11
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
|
12
|
+
# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
13
|
+
# COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
14
|
+
# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
15
|
+
#
|
16
|
+
module Gruf
|
17
|
+
class Client < SimpleDelegator
|
18
|
+
##
|
19
|
+
# Translates exceptions into Gruf::Client::Errors
|
20
|
+
#
|
21
|
+
class ErrorFactory
|
22
|
+
##
|
23
|
+
# @param [Class] default_class
|
24
|
+
# @param [Class] deserializer_class
|
25
|
+
# @param [String|Symbol] metadata_key
|
26
|
+
#
|
27
|
+
def initialize(
|
28
|
+
default_class: nil,
|
29
|
+
deserializer_class: nil,
|
30
|
+
metadata_key: nil
|
31
|
+
)
|
32
|
+
@default_class = default_class || Gruf::Client::Errors::Internal
|
33
|
+
@metadata_key = (metadata_key || Gruf.error_metadata_key).to_s
|
34
|
+
default_serializer = if Gruf.error_serializer
|
35
|
+
Gruf.error_serializer.is_a?(Class) ? Gruf.error_serializer : Gruf.error_serializer.to_s.constantize
|
36
|
+
else
|
37
|
+
Gruf::Serializers::Errors::Json
|
38
|
+
end
|
39
|
+
@deserializer_class = deserializer_class || default_serializer
|
40
|
+
end
|
41
|
+
|
42
|
+
##
|
43
|
+
# Determine the proper error class to raise given the incoming exception. This will attempt to coalesce the
|
44
|
+
# exception object into the appropriate Gruf::Client::Errors subclass, or fallback to the default class if none
|
45
|
+
# is found (or it is a StandardError or higher-level error). It will leave alone Signals instead of attempting to
|
46
|
+
# coalesce them.
|
47
|
+
#
|
48
|
+
# @param [Exception] exception
|
49
|
+
# @return [Gruf::Client::Errors::Base|SignalException]
|
50
|
+
#
|
51
|
+
def from_exception(exception)
|
52
|
+
# passthrough on Signals, we don't want to mess with these
|
53
|
+
return exception if exception.is_a?(SignalException)
|
54
|
+
|
55
|
+
exception_class = determine_class(exception)
|
56
|
+
if exception.is_a?(GRPC::BadStatus)
|
57
|
+
# if it's a GRPC::BadStatus code, let's check for any trailing error metadata and decode it
|
58
|
+
exception_class.new(deserialize(exception))
|
59
|
+
else
|
60
|
+
# otherwise, let's just capture the error and build the wrapper class
|
61
|
+
exception_class.new(exception)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
private
|
66
|
+
|
67
|
+
##
|
68
|
+
# Deserialize any trailing metadata error payload from the exception
|
69
|
+
#
|
70
|
+
# @param [Gruf::Client::Errors::Base]
|
71
|
+
# @return [String]
|
72
|
+
#
|
73
|
+
def deserialize(exception)
|
74
|
+
if exception.respond_to?(:metadata)
|
75
|
+
key = exception.metadata.key?(@metadata_key.to_s) ? @metadata_key.to_s : @metadata_key.to_sym
|
76
|
+
return @deserializer_class.new(exception.metadata[key]).deserialize if exception.metadata.key?(key)
|
77
|
+
end
|
78
|
+
|
79
|
+
exception
|
80
|
+
end
|
81
|
+
|
82
|
+
##
|
83
|
+
# @param [Exception] exception
|
84
|
+
# @return [Gruf::Client::Errors::Base]
|
85
|
+
#
|
86
|
+
def determine_class(exception)
|
87
|
+
error_class = Gruf::Client::Errors.const_get(exception.class.name.demodulize)
|
88
|
+
|
89
|
+
# Ruby module inheritance will have StandardError, ScriptError, etc still get to this point
|
90
|
+
# So we need to explicitly check for ancestry here
|
91
|
+
return @default_class unless error_class.ancestors.include?(Gruf::Client::Errors::Base)
|
92
|
+
|
93
|
+
error_class
|
94
|
+
rescue NameError => _
|
95
|
+
@default_class
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
data/lib/gruf/configuration.rb
CHANGED
@@ -40,6 +40,7 @@ module Gruf
|
|
40
40
|
use_exception_message: true,
|
41
41
|
internal_error_message: 'Internal Server Error',
|
42
42
|
event_listener_proc: nil,
|
43
|
+
synchronized_client_internal_cache_expiry: 60,
|
43
44
|
rpc_server_options: {
|
44
45
|
pool_size: GRPC::RpcServer::DEFAULT_POOL_SIZE,
|
45
46
|
max_waiting_requests: GRPC::RpcServer::DEFAULT_MAX_WAITING_REQUESTS,
|
@@ -78,7 +78,7 @@ module Gruf
|
|
78
78
|
end
|
79
79
|
|
80
80
|
payload = {}
|
81
|
-
if !request.client_streamer?
|
81
|
+
if !request.client_streamer? && !request.bidi_streamer?
|
82
82
|
payload[:params] = sanitize(request.message.to_h) if options.fetch(:log_parameters, false)
|
83
83
|
payload[:message] = message(request, result)
|
84
84
|
payload[:status] = status(result.message, result.successful?)
|
@@ -87,6 +87,7 @@ module Gruf
|
|
87
87
|
payload[:message] = ''
|
88
88
|
payload[:status] = GRPC::Core::StatusCodes::OK
|
89
89
|
end
|
90
|
+
|
90
91
|
payload[:service] = request.service_key
|
91
92
|
payload[:method] = request.method_key
|
92
93
|
payload[:action] = request.method_key
|
data/lib/gruf/response.rb
CHANGED
@@ -35,11 +35,12 @@ module Gruf
|
|
35
35
|
# Initialize a response object with the given gRPC operation
|
36
36
|
#
|
37
37
|
# @param [GRPC::ActiveCall::Operation] operation The given operation for the current call
|
38
|
+
# @param [StdClass] message
|
38
39
|
# @param [Float] execution_time The amount of time that the response took to occur
|
39
40
|
#
|
40
|
-
def initialize(operation
|
41
|
+
def initialize(operation:, message:, execution_time: nil)
|
41
42
|
@operation = operation
|
42
|
-
@message =
|
43
|
+
@message = message
|
43
44
|
@metadata = operation.metadata
|
44
45
|
@trailing_metadata = operation.trailing_metadata
|
45
46
|
@deadline = operation.deadline
|
@@ -0,0 +1,93 @@
|
|
1
|
+
# Copyright (c) 2017-present, BigCommerce Pty. Ltd. All rights reserved
|
2
|
+
#
|
3
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
|
4
|
+
# documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
|
5
|
+
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit
|
6
|
+
# persons to whom the Software is furnished to do so, subject to the following conditions:
|
7
|
+
#
|
8
|
+
# The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
|
9
|
+
# Software.
|
10
|
+
#
|
11
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
|
12
|
+
# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
13
|
+
# COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
14
|
+
# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
15
|
+
#
|
16
|
+
require 'concurrent'
|
17
|
+
|
18
|
+
module Gruf
|
19
|
+
##
|
20
|
+
# Ensures that we only have one active call to a given endpoint with a given set of params. This can be useful
|
21
|
+
# to mitigate thundering herds.
|
22
|
+
#
|
23
|
+
class SynchronizedClient < Gruf::Client
|
24
|
+
attr_reader :unsynchronized_methods
|
25
|
+
|
26
|
+
##
|
27
|
+
# Initialize the client and setup the stub
|
28
|
+
#
|
29
|
+
# @param [Module] service The namespace of the client Stub that is desired to load
|
30
|
+
# @param [Hash] options A hash of options for the client
|
31
|
+
# @option options [Array] :unsynchronized_methods A list of methods (as symbols) that should be excluded from synchronization
|
32
|
+
# @option options [Integer] :internal_cache_expiry The length of time to keep results around for other threads to fetch (in seconds)
|
33
|
+
# @param [Hash] client_options A hash of options to pass to the gRPC client stub
|
34
|
+
#
|
35
|
+
def initialize(service:, options: {}, client_options: {})
|
36
|
+
@unsynchronized_methods = options.delete(:unsynchronized_methods) { [] }
|
37
|
+
@expiry = options.delete(:internal_cache_expiry) { Gruf.synchronized_client_internal_cache_expiry }
|
38
|
+
@locks = Concurrent::Map.new
|
39
|
+
@results = Concurrent::Map.new
|
40
|
+
super
|
41
|
+
end
|
42
|
+
|
43
|
+
##
|
44
|
+
# Call the client's method with given params. If another call is already active for the same endpoint and the same
|
45
|
+
# params, block until the active call is complete. When unblocked, callers will get a copy of the original result.
|
46
|
+
#
|
47
|
+
# @param [String|Symbol] request_method The method that is being requested on the service
|
48
|
+
# @param [Hash] params (Optional) A hash of parameters that will be inserted into the gRPC request message that is required
|
49
|
+
# for the given above call
|
50
|
+
# @param [Hash] metadata (Optional) A hash of metadata key/values that are transported with the client request
|
51
|
+
# @param [Hash] opts (Optional) A hash of options to send to the gRPC request_response method
|
52
|
+
# @return [Gruf::Response] The response from the server
|
53
|
+
# @raise [Gruf::Client::Error|GRPC::BadStatus] If an error occurs, an exception will be raised according to the
|
54
|
+
# error type that was returned
|
55
|
+
#
|
56
|
+
def call(request_method, params = {}, metadata = {}, opts = {}, &block)
|
57
|
+
# Allow for bypassing extra behavior for selected methods
|
58
|
+
return super if unsynchronized_methods.include?(request_method.to_sym)
|
59
|
+
|
60
|
+
# Generate a unique key based on the method and params
|
61
|
+
key = "#{request_method}.#{params.hash}"
|
62
|
+
|
63
|
+
# Create a lock for this call if we haven't seen it already, then acquire it
|
64
|
+
lock = @locks.compute_if_absent(key) { Mutex.new }
|
65
|
+
lock.synchronize do
|
66
|
+
# Return value from results cache if it exists. This occurs for callers that were
|
67
|
+
# waiting on the lock while the first caller was making the actual grpc call.
|
68
|
+
response = @results.get(lock)
|
69
|
+
if response
|
70
|
+
Gruf.logger.debug "Returning cached result for #{key}:#{lock.inspect}"
|
71
|
+
next response
|
72
|
+
end
|
73
|
+
|
74
|
+
# Make the grpc call and record response for other callers that are blocked
|
75
|
+
# on the same lock
|
76
|
+
response = super
|
77
|
+
@results.put(lock, response)
|
78
|
+
|
79
|
+
# Schedule a task to come in later and clean out result to prevent memory bloat
|
80
|
+
Concurrent::ScheduledTask.new(@expiry, args: [@results, lock]) { |h, k| h.delete(k) }.execute
|
81
|
+
|
82
|
+
# Remove the lock from the map. The next caller to come through with the
|
83
|
+
# same params will create a new lock and start the process over again.
|
84
|
+
# Anyone who was waiting on this call will be using a local reference
|
85
|
+
# to the same lock as us, and will fetch the result from the cache.
|
86
|
+
@locks.delete(key)
|
87
|
+
|
88
|
+
# Return response
|
89
|
+
response
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
data/lib/gruf/timer.rb
CHANGED
@@ -50,7 +50,7 @@ module Gruf
|
|
50
50
|
# @return [Boolean] Whether or not this result was a success
|
51
51
|
#
|
52
52
|
def success?
|
53
|
-
!result.is_a?(GRPC::BadStatus)
|
53
|
+
!result.is_a?(GRPC::BadStatus) && !result.is_a?(StandardError) && !result.is_a?(GRPC::Core::CallError)
|
54
54
|
end
|
55
55
|
end
|
56
56
|
|
@@ -65,7 +65,7 @@ module Gruf
|
|
65
65
|
start_time = Time.now
|
66
66
|
begin
|
67
67
|
result = yield
|
68
|
-
rescue GRPC::BadStatus => e
|
68
|
+
rescue GRPC::BadStatus, StandardError, GRPC::Core::CallError => e
|
69
69
|
result = e
|
70
70
|
end
|
71
71
|
end_time = Time.now
|
data/lib/gruf/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: gruf
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.
|
4
|
+
version: 2.5.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Shaun McCormick
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2018-09-
|
11
|
+
date: 2018-09-14 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -94,6 +94,20 @@ dependencies:
|
|
94
94
|
- - ">="
|
95
95
|
- !ruby/object:Gem::Version
|
96
96
|
version: '0'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: concurrent-ruby
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
type: :runtime
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
97
111
|
- !ruby/object:Gem::Dependency
|
98
112
|
name: slop
|
99
113
|
requirement: !ruby/object:Gem::Requirement
|
@@ -124,6 +138,8 @@ files:
|
|
124
138
|
- lib/gruf.rb
|
125
139
|
- lib/gruf/cli/executor.rb
|
126
140
|
- lib/gruf/client.rb
|
141
|
+
- lib/gruf/client/error.rb
|
142
|
+
- lib/gruf/client/error_factory.rb
|
127
143
|
- lib/gruf/configuration.rb
|
128
144
|
- lib/gruf/controllers/base.rb
|
129
145
|
- lib/gruf/controllers/request.rb
|
@@ -154,6 +170,7 @@ files:
|
|
154
170
|
- lib/gruf/serializers/errors/base.rb
|
155
171
|
- lib/gruf/serializers/errors/json.rb
|
156
172
|
- lib/gruf/server.rb
|
173
|
+
- lib/gruf/synchronized_client.rb
|
157
174
|
- lib/gruf/timer.rb
|
158
175
|
- lib/gruf/version.rb
|
159
176
|
homepage: https://github.com/bigcommerce/gruf
|