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 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