px-service-client 1.0.1 → 1.0.4
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 +4 -4
- data/README.md +11 -19
- data/lib/px/service/client/base.rb +5 -4
- data/lib/px/service/client/circuit_breaker.rb +38 -23
- data/lib/px/service/client/future.rb +11 -0
- data/lib/px/service/client/version.rb +1 -1
- data/px-service-client.gemspec +1 -0
- data/spec/px/service/client/circuit_breaker_spec.rb +124 -32
- data/spec/px/service/client/future_spec.rb +48 -1
- data/spec/spec_helper.rb +1 -0
- metadata +17 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 31469d2437619f1f3afec21f42201aa2054ade9b
|
4
|
+
data.tar.gz: 55491249709bd72ab3e90fa9648cb72dee46880b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: bb3b61faa7e47e838f45db03731effe82ef5c92ca109878bd592ab80869ab8a00706c611dd586d253078cd9c01b48ae28dda04fd650c71a1cfdefe2dafd8419d
|
7
|
+
data.tar.gz: b86dff5d8658f923ae59494edc8314dd2eb371b2e4757ace908598a106c084f4c02ffa878cd05a882bbcc4056df4cbd7f93335d03f67de75b25aaef6415c9bfd
|
data/README.md
CHANGED
@@ -24,8 +24,8 @@ Then use it:
|
|
24
24
|
require 'px-service-client'
|
25
25
|
|
26
26
|
class MyClient
|
27
|
-
include Px::Service::Client
|
28
|
-
include Px::Service::Client
|
27
|
+
include Px::Service::Client::Caching
|
28
|
+
include Px::Service::Client::CircuitBreaker
|
29
29
|
end
|
30
30
|
|
31
31
|
```
|
@@ -37,10 +37,10 @@ This gem includes several common features used in 500px service client libraries
|
|
37
37
|
|
38
38
|
The features are:
|
39
39
|
|
40
|
-
#### Px::Service::Client
|
40
|
+
#### Px::Service::Client::Caching
|
41
41
|
|
42
42
|
```ruby
|
43
|
-
include Px::Service::Client
|
43
|
+
include Px::Service::Client::Caching
|
44
44
|
|
45
45
|
self.cache_client = Dalli::Client.new(...)
|
46
46
|
self.cache_logger = Logger.new(STDOUT) # or Rails.logger, for example
|
@@ -55,13 +55,9 @@ to be refreshed probabilistically (rather than on every request).
|
|
55
55
|
*first-resort* means that the cached value is always used, if present. Requests to the service are only made
|
56
56
|
when the cached value is close to expiry.
|
57
57
|
|
58
|
-
#### Px::Service::Client
|
58
|
+
#### Px::Service::Client::CircuitBreaker
|
59
59
|
|
60
60
|
```ruby
|
61
|
-
def call_remote_service() ...
|
62
|
-
|
63
|
-
circuit_method :call_remote_service
|
64
|
-
|
65
61
|
# Optional
|
66
62
|
circuit_handler do |handler|
|
67
63
|
handler.logger = Logger.new(STDOUT)
|
@@ -72,25 +68,21 @@ circuit_handler do |handler|
|
|
72
68
|
end
|
73
69
|
```
|
74
70
|
|
75
|
-
|
76
|
-
`circuit_method` will be wrapped in a circuit breaker that will raise a `Px::Service::ServiceError` if the breaker
|
77
|
-
is open. The circuit will open on any exception from the wrapped method, or if the wrapped method
|
78
|
-
runs for longer than the `invocation_timeout`.
|
71
|
+
Adds a circuit breaker to the client. The circuit will open on any exception from the wrapped method, or if the request runs for longer than the `invocation_timeout`.
|
79
72
|
|
80
|
-
Note that `Px::Service::ServiceRequestError` exceptions do NOT trip the breaker, as these exceptions indicate an error
|
81
|
-
on the caller's part (e.g. an HTTP 4xx error).
|
73
|
+
Note that `Px::Service::ServiceRequestError` exceptions do NOT trip the breaker, as these exceptions indicate an error on the caller's part (e.g. an HTTP 4xx error).
|
82
74
|
|
83
|
-
|
84
|
-
|
75
|
+
Every instance of the class that includes the `CircuitBreaker` concern will share the same circuit state. You should therefore include `Px::Service::Client::CircuitBreaker` in the most-derived class that subclasses
|
76
|
+
`Px::Service::Client::Base`
|
85
77
|
|
86
78
|
This module is based on (and uses) the [Circuit Breaker](https://github.com/wsargent/circuit_breaker) gem by Will Sargent.
|
87
79
|
|
88
|
-
#### Px::Service::Client
|
80
|
+
#### Px::Service::Client::ListResponse
|
89
81
|
|
90
82
|
```ruby
|
91
83
|
def get_something(page, page_size)
|
92
84
|
response = JSON.parse(http_get("http://some/url?p=#{page}&l=#{page_size}"))
|
93
|
-
return Px::Service::Client
|
85
|
+
return Px::Service::Client::ListResponse(page_size, response, "items")
|
94
86
|
end
|
95
87
|
|
96
88
|
```
|
@@ -1,7 +1,6 @@
|
|
1
1
|
module Px::Service::Client
|
2
2
|
class Base
|
3
3
|
include Px::Service::Client::Caching
|
4
|
-
include Px::Service::Client::CircuitBreaker
|
5
4
|
cattr_accessor :logger
|
6
5
|
|
7
6
|
private
|
@@ -18,13 +17,16 @@ module Px::Service::Client
|
|
18
17
|
end
|
19
18
|
end
|
20
19
|
|
21
|
-
|
20
|
+
##
|
21
|
+
# Make the request
|
22
|
+
def make_request(method, uri, query: nil, headers: nil, body: nil, timeout: 0)
|
22
23
|
req = Typhoeus::Request.new(
|
23
24
|
uri,
|
24
25
|
method: method,
|
25
26
|
params: query,
|
26
27
|
body: body,
|
27
|
-
headers: headers
|
28
|
+
headers: headers,
|
29
|
+
timeout: timeout)
|
28
30
|
|
29
31
|
start_time = Time.now
|
30
32
|
logger.debug "Making request #{method.to_s.upcase} #{uri}" if logger
|
@@ -36,6 +38,5 @@ module Px::Service::Client
|
|
36
38
|
|
37
39
|
RetriableResponseFuture.new(req)
|
38
40
|
end
|
39
|
-
|
40
41
|
end
|
41
42
|
end
|
@@ -3,10 +3,9 @@ require 'circuit_breaker'
|
|
3
3
|
module Px::Service::Client
|
4
4
|
module CircuitBreaker
|
5
5
|
extend ActiveSupport::Concern
|
6
|
+
include ::CircuitBreaker
|
6
7
|
|
7
8
|
included do
|
8
|
-
include ::CircuitBreaker
|
9
|
-
|
10
9
|
# Default circuit breaker configuration. Can be overridden
|
11
10
|
circuit_handler do |handler|
|
12
11
|
handler.failure_threshold = 5
|
@@ -15,33 +14,49 @@ module Px::Service::Client
|
|
15
14
|
handler.excluded_exceptions = [Px::Service::ServiceRequestError]
|
16
15
|
end
|
17
16
|
|
18
|
-
|
19
|
-
|
17
|
+
cattr_accessor :circuit_state do
|
18
|
+
::CircuitBreaker::CircuitState.new
|
20
19
|
end
|
21
|
-
end
|
22
20
|
|
21
|
+
alias_method_chain :make_request, :breaker
|
22
|
+
end
|
23
23
|
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
rescue StandardError => ex
|
39
|
-
# Wrap other exceptions, includes CircuitBreaker::CircuitBrokenException
|
40
|
-
raise Px::Service::ServiceError.new(ex.message, 503), ex, ex.backtrace
|
41
|
-
end
|
24
|
+
##
|
25
|
+
# Make the request, respecting the circuit breaker, if configured
|
26
|
+
def make_request_with_breaker(method, uri, query: nil, headers: nil, body: nil)
|
27
|
+
Future.new do
|
28
|
+
state = self.class.circuit_state
|
29
|
+
handler = self.class.circuit_handler
|
30
|
+
|
31
|
+
if handler.is_tripped(state)
|
32
|
+
handler.logger.debug("handle: breaker is tripped, refusing to execute: #{state}") if handler.logger
|
33
|
+
begin
|
34
|
+
handler.on_circuit_open(state)
|
35
|
+
rescue StandardError => ex
|
36
|
+
# Wrap and reroute other exceptions, includes CircuitBreaker::CircuitBrokenException
|
37
|
+
raise Px::Service::ServiceError.new(ex.message, 503), ex, ex.backtrace
|
42
38
|
end
|
43
39
|
end
|
40
|
+
|
41
|
+
begin
|
42
|
+
response = make_request_without_breaker(
|
43
|
+
method,
|
44
|
+
uri,
|
45
|
+
query: query,
|
46
|
+
headers: headers,
|
47
|
+
body: body,
|
48
|
+
timeout: handler.invocation_timeout)
|
49
|
+
|
50
|
+
result = response.value!
|
51
|
+
handler.on_success(state)
|
52
|
+
|
53
|
+
result
|
54
|
+
rescue Px::Service::ServiceError
|
55
|
+
handler.on_failure(state)
|
56
|
+
raise
|
57
|
+
end
|
44
58
|
end
|
45
59
|
end
|
60
|
+
|
46
61
|
end
|
47
62
|
end
|
@@ -56,6 +56,17 @@ module Px::Service::Client
|
|
56
56
|
end
|
57
57
|
end
|
58
58
|
|
59
|
+
def value!
|
60
|
+
if @completed
|
61
|
+
result = @value
|
62
|
+
else
|
63
|
+
result = wait_for_value(nil)
|
64
|
+
end
|
65
|
+
raise result if result.kind_of?(Exception)
|
66
|
+
|
67
|
+
result
|
68
|
+
end
|
69
|
+
|
59
70
|
def completed?
|
60
71
|
@completed
|
61
72
|
end
|
data/px-service-client.gemspec
CHANGED
@@ -26,6 +26,7 @@ Gem::Specification.new do |spec|
|
|
26
26
|
spec.add_development_dependency "bundler", "~> 1.6"
|
27
27
|
spec.add_development_dependency "rake"
|
28
28
|
spec.add_development_dependency "webmock"
|
29
|
+
spec.add_development_dependency "pry"
|
29
30
|
spec.add_development_dependency "pry-byebug"
|
30
31
|
spec.add_development_dependency "vcr"
|
31
32
|
spec.add_development_dependency "guard"
|
@@ -1,11 +1,17 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
3
|
describe Px::Service::Client::CircuitBreaker do
|
4
|
-
|
5
4
|
let(:subject_class) {
|
6
|
-
Class.new
|
5
|
+
Class.new do
|
6
|
+
def make_request(method, uri, query: nil, headers: nil, body: nil, timeout: 0)
|
7
|
+
Px::Service::Client::Future.new do
|
8
|
+
_result
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end.tap do |c|
|
7
12
|
# Anonymous classes don't have a name. Stub out :name so that things work
|
8
13
|
allow(c).to receive(:name).and_return("CircuitBreaker")
|
14
|
+
c.include(Px::Service::Client::CircuitBreaker)
|
9
15
|
end
|
10
16
|
}
|
11
17
|
|
@@ -29,85 +35,171 @@ describe Px::Service::Client::CircuitBreaker do
|
|
29
35
|
end
|
30
36
|
end
|
31
37
|
|
32
|
-
describe '#
|
33
|
-
context "when the
|
38
|
+
describe '#make_request' do
|
39
|
+
context "when the underlying request method succeeds" do
|
34
40
|
before :each do
|
35
|
-
subject_class.send(:define_method, :
|
36
|
-
"returned
|
41
|
+
subject_class.send(:define_method, :_result) do
|
42
|
+
"returned test"
|
37
43
|
end
|
44
|
+
end
|
38
45
|
|
39
|
-
|
46
|
+
it "returns a Future" do
|
47
|
+
expect(subject.make_request(:get, "http://test")).to be_a_kind_of(Px::Service::Client::Future)
|
40
48
|
end
|
41
49
|
|
42
50
|
it "returns the return value" do
|
43
|
-
expect(subject.
|
51
|
+
expect(subject.make_request(:get, "http://test").value!).to eq("returned test")
|
52
|
+
end
|
53
|
+
|
54
|
+
context "when the breaker is open" do
|
55
|
+
before :each do
|
56
|
+
allow(subject_class.circuit_handler).to receive(:is_timeout_exceeded).and_return(true)
|
57
|
+
|
58
|
+
subject.circuit_state.trip
|
59
|
+
subject.circuit_state.last_failure_time = Time.now
|
60
|
+
subject.circuit_state.failure_count = 5
|
61
|
+
end
|
62
|
+
|
63
|
+
it "resets the failure count of the breaker" do
|
64
|
+
expect {
|
65
|
+
subject.make_request(:get, "http://test").value!
|
66
|
+
}.to change{subject.class.circuit_state.failure_count}.to(0)
|
67
|
+
end
|
68
|
+
|
69
|
+
it "closes the breaker" do
|
70
|
+
expect {
|
71
|
+
subject.make_request(:get, "http://test").value!
|
72
|
+
}.to change{subject.class.circuit_state.closed?}.from(false).to(true)
|
73
|
+
end
|
44
74
|
end
|
45
75
|
end
|
46
76
|
|
47
77
|
context "when the wrapped method fails with a ServiceRequestError" do
|
48
78
|
before :each do
|
49
|
-
subject_class.send(:define_method, :
|
79
|
+
subject_class.send(:define_method, :_result) do
|
50
80
|
raise Px::Service::ServiceRequestError.new("Error", 404)
|
51
81
|
end
|
52
|
-
|
53
|
-
subject_class.circuit_method(:test_method)
|
54
82
|
end
|
55
83
|
|
56
84
|
it "raises a ServiceRequestError" do
|
57
|
-
expect{
|
58
|
-
subject.
|
85
|
+
expect {
|
86
|
+
subject.make_request(:get, "http://test").value!
|
59
87
|
}.to raise_error(Px::Service::ServiceRequestError, "Error")
|
60
88
|
end
|
89
|
+
|
90
|
+
it "does not increment the failure count of the breaker" do
|
91
|
+
expect {
|
92
|
+
subject.make_request(:get, "http://test").value! rescue nil
|
93
|
+
}.not_to change{subject.class.circuit_state.failure_count}
|
94
|
+
end
|
61
95
|
end
|
62
96
|
|
63
97
|
context "when the wrapped method fails with a ServiceError" do
|
64
98
|
before :each do
|
65
|
-
subject_class.send(:define_method, :
|
99
|
+
subject_class.send(:define_method, :_result) do
|
66
100
|
raise Px::Service::ServiceError.new("Error", 500)
|
67
101
|
end
|
68
|
-
|
69
|
-
subject_class.circuit_method(:test_method)
|
70
102
|
end
|
71
103
|
|
72
104
|
it "raises a ServiceError" do
|
73
|
-
expect{
|
74
|
-
subject.
|
105
|
+
expect {
|
106
|
+
subject.make_request(:get, "http://test").value!
|
75
107
|
}.to raise_error(Px::Service::ServiceError, "Error")
|
76
108
|
end
|
109
|
+
|
110
|
+
it "increments the failure count of the breaker" do
|
111
|
+
expect {
|
112
|
+
subject.make_request(:get, "http://test").value! rescue nil
|
113
|
+
}.to change{subject.class.circuit_state.failure_count}.by(1)
|
114
|
+
end
|
77
115
|
end
|
78
116
|
|
79
|
-
context "when the
|
117
|
+
context "when the circuit is open" do
|
80
118
|
before :each do
|
81
|
-
subject_class.send(:define_method, :
|
82
|
-
|
119
|
+
subject_class.send(:define_method, :_result) do
|
120
|
+
"should not be called"
|
83
121
|
end
|
84
122
|
|
85
|
-
|
123
|
+
subject.circuit_state.trip
|
124
|
+
subject.circuit_state.last_failure_time = Time.now
|
86
125
|
end
|
87
126
|
|
88
127
|
it "raises a ServiceError" do
|
89
|
-
expect{
|
90
|
-
subject.
|
128
|
+
expect {
|
129
|
+
subject.make_request(:get, "http://test").value!
|
91
130
|
}.to raise_error(Px::Service::ServiceError)
|
92
131
|
end
|
93
132
|
end
|
94
133
|
|
95
|
-
context "
|
134
|
+
context "with multiple classes" do
|
135
|
+
let(:other_class) {
|
136
|
+
Class.new do
|
137
|
+
def make_request(method, uri, query: nil, headers: nil, body: nil, timeout: 0)
|
138
|
+
Px::Service::Client::Future.new do
|
139
|
+
"result"
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end.tap do |c|
|
143
|
+
# Anonymous classes don't have a name. Stub out :name so that things work
|
144
|
+
allow(c).to receive(:name).and_return("OtherCircuitBreaker")
|
145
|
+
c.include(Px::Service::Client::CircuitBreaker)
|
146
|
+
end
|
147
|
+
}
|
148
|
+
|
149
|
+
let(:other) { other_class.new }
|
150
|
+
|
96
151
|
before :each do
|
97
|
-
subject_class.send(:define_method, :
|
152
|
+
subject_class.send(:define_method, :_result) do
|
98
153
|
"should not be called"
|
99
154
|
end
|
155
|
+
end
|
100
156
|
|
101
|
-
|
157
|
+
context "when the breaker opens on the first instance" do
|
158
|
+
before :each do
|
159
|
+
subject.circuit_state.trip
|
160
|
+
subject.circuit_state.last_failure_time = Time.now
|
161
|
+
end
|
102
162
|
|
103
|
-
|
163
|
+
it "raises a ServiceError on the first instance" do
|
164
|
+
expect {
|
165
|
+
subject.make_request(:get, "http://test").value!
|
166
|
+
}.to raise_error(Px::Service::ServiceError)
|
167
|
+
end
|
168
|
+
|
169
|
+
it "does not raise a ServiceError on the second instance" do
|
170
|
+
expect(other.make_request(:get, "http://test").value!).to eq("result")
|
171
|
+
end
|
104
172
|
end
|
173
|
+
end
|
105
174
|
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
175
|
+
context "with multiple instances of the same class" do
|
176
|
+
let(:other) { subject_class.new }
|
177
|
+
|
178
|
+
before :each do
|
179
|
+
subject_class.send(:define_method, :_result) do
|
180
|
+
"should not be called"
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
context "when the breaker opens on the first instance" do
|
185
|
+
before :each do
|
186
|
+
subject.circuit_state.trip
|
187
|
+
subject.circuit_state.last_failure_time = Time.now
|
188
|
+
end
|
189
|
+
|
190
|
+
it "raises a ServiceError on the first instance" do
|
191
|
+
expect {
|
192
|
+
subject.make_request(:get, "http://test").value!
|
193
|
+
}.to raise_error(Px::Service::ServiceError)
|
194
|
+
end
|
195
|
+
|
196
|
+
it "raises a ServiceError on the second instance" do
|
197
|
+
expect {
|
198
|
+
other.make_request(:get, "http://test").value!
|
199
|
+
}.to raise_error(Px::Service::ServiceError)
|
200
|
+
end
|
110
201
|
end
|
111
202
|
end
|
203
|
+
|
112
204
|
end
|
113
205
|
end
|
@@ -46,7 +46,7 @@ describe Px::Service::Client::Future do
|
|
46
46
|
it "does not call the method on the value" do
|
47
47
|
called = false
|
48
48
|
Fiber.new do
|
49
|
-
subject.
|
49
|
+
subject.value
|
50
50
|
called = true
|
51
51
|
end.resume
|
52
52
|
|
@@ -86,6 +86,53 @@ describe Px::Service::Client::Future do
|
|
86
86
|
end
|
87
87
|
end
|
88
88
|
|
89
|
+
describe '#value!' do
|
90
|
+
context "when the future is not complete" do
|
91
|
+
it "does not call the method on the value" do
|
92
|
+
called = false
|
93
|
+
Fiber.new do
|
94
|
+
subject.value!
|
95
|
+
called = true
|
96
|
+
end.resume
|
97
|
+
|
98
|
+
expect(called).to eq(false)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
context "when the future is already complete" do
|
103
|
+
it "returns the value" do
|
104
|
+
subject.complete(value)
|
105
|
+
expect(subject.value!).to eq(value)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
context "when the value is an exception" do
|
110
|
+
it "returns the exception" do
|
111
|
+
Fiber.new do
|
112
|
+
expect {
|
113
|
+
subject.value!
|
114
|
+
}.to raise_error(ArgumentError)
|
115
|
+
end.resume
|
116
|
+
|
117
|
+
Fiber.new do
|
118
|
+
subject.complete(ArgumentError.new("Error"))
|
119
|
+
end.resume
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
context "when the method returns a value" do
|
124
|
+
it "returns the value" do
|
125
|
+
Fiber.new do
|
126
|
+
expect(subject.value!).to eq(value)
|
127
|
+
end.resume
|
128
|
+
|
129
|
+
Fiber.new do
|
130
|
+
subject.complete(value)
|
131
|
+
end.resume
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
89
136
|
describe '#method_missing' do
|
90
137
|
context "when the future is already complete" do
|
91
138
|
context "when the method raised an exception" do
|
data/spec/spec_helper.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: px-service-client
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0.
|
4
|
+
version: 1.0.4
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Chris Micacchi
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2015-08-04 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: will_paginate
|
@@ -122,6 +122,20 @@ dependencies:
|
|
122
122
|
- - ">="
|
123
123
|
- !ruby/object:Gem::Version
|
124
124
|
version: '0'
|
125
|
+
- !ruby/object:Gem::Dependency
|
126
|
+
name: pry
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - ">="
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: '0'
|
132
|
+
type: :development
|
133
|
+
prerelease: false
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - ">="
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: '0'
|
125
139
|
- !ruby/object:Gem::Dependency
|
126
140
|
name: pry-byebug
|
127
141
|
requirement: !ruby/object:Gem::Requirement
|
@@ -269,7 +283,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
269
283
|
version: '0'
|
270
284
|
requirements: []
|
271
285
|
rubyforge_project:
|
272
|
-
rubygems_version: 2.
|
286
|
+
rubygems_version: 2.4.5
|
273
287
|
signing_key:
|
274
288
|
specification_version: 4
|
275
289
|
summary: Common service client behaviours for Ruby applications
|