gruf 2.4.2 → 2.5.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bef9466a137b8d52bc76a4d24c65fef26b0701cff21eefb2ae9811e020dd619c
4
- data.tar.gz: f3209bfefd0e3a7fe7ee03b6bcc7a417974593b924966521346d095d87de6d5d
3
+ metadata.gz: a2452f173e2987612fde71b5e170874f52718e5a7f820658378c7a98e54f5b1f
4
+ data.tar.gz: bcbb20ab199f78fadd1926d1314fe881fccda9413f149ebb8ad1e4daeba259f8
5
5
  SHA512:
6
- metadata.gz: 906fa455028407ea0ac54dceeb89c54b032ddf9e1090e85d14dabf8496aad91b22d19056fdf8e84d9836399518198ed7afffa50c1cc69ac9d8a4e5a43e53f40b
7
- data.tar.gz: c3ec65443321ff6ac00b839bf3b6a9fed748e5f7ab2ec73ffd4476d1b4b7590ccd2d865ac4f5c6703a59c5ae89746b2964f58ee43e56c62a3df62e8bddae9271
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 client options, like so:
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
- Timer.time do
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
@@ -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, execution_time = nil)
41
+ def initialize(operation:, message:, execution_time: nil)
41
42
  @operation = operation
42
- @message = operation.execute
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
@@ -14,5 +14,5 @@
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
16
  module Gruf
17
- VERSION = '2.4.2'.freeze
17
+ VERSION = '2.5.0'.freeze
18
18
  end
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.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-07 00:00:00.000000000 Z
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