px-service-client 1.2.0 → 1.2.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +96 -24
- data/lib/px/service/client/base.rb +2 -1
- data/lib/px/service/client/hmac_signing.rb +67 -0
- data/lib/px/service/client/version.rb +1 -1
- data/lib/px/service/client.rb +3 -0
- data/spec/px/service/client/hmac_signing_spec.rb +45 -0
- metadata +5 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e91b9e1a396db568b3b30155336363538b80fff6
|
4
|
+
data.tar.gz: 72792c99dc994aeed39b1e0a0b586c486bc96e03
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1380aa22511673fc12aef1b7e67d165e002429793d6aefebd6786c9d1f0f65c1db2ec80c9921c6b5382936aacfb3fd437644b7c7a8ceac2ab41c7132aef36bb7
|
7
|
+
data.tar.gz: 45a304051466c4540fbc41dfe21efc454d973e119f4d8637cbce0c947fc213dc3980ea4289fef4f2368d385150afbe01c47d8b91ea19c580aa919c16f74d6604
|
data/README.md
CHANGED
@@ -23,7 +23,7 @@ Then use it:
|
|
23
23
|
```ruby
|
24
24
|
require 'px-service-client'
|
25
25
|
|
26
|
-
class MyClient
|
26
|
+
class MyClient < Px::Service::Client::Base
|
27
27
|
include Px::Service::Client::Caching
|
28
28
|
include Px::Service::Client::CircuitBreaker
|
29
29
|
end
|
@@ -37,8 +37,41 @@ 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::Base
|
41
|
+
This class provides a basic `make_request(method, url, ...)` method that produces an asynchronous request. The method immediately returns a `RetriableResponseFuture`. It works together with `Multiplexer`(discussed below) and uses [Typhoeus](https://github.com/typhoeus/typhoeus) as the underlying HTTP client to support asynchronicity.
|
42
|
+
|
43
|
+
**Customized clients usually inherit this class and include other features/mixins, if needed.**
|
44
|
+
|
45
|
+
See the following secion for an example of how to use `make_request` and `Multiplexer`.
|
46
|
+
|
47
|
+
#### Px::Service::Client::Multiplexer
|
48
|
+
This class works together with `Px::Service::Client::Base` sub-classes to support request parallel execution.
|
49
|
+
|
50
|
+
Example:
|
51
|
+
|
52
|
+
```Ruby
|
53
|
+
multi = Px::Service::Client::Multiplexer.new
|
54
|
+
|
55
|
+
multi.context do
|
56
|
+
method = :get
|
57
|
+
url = 'http://www.example.com'
|
58
|
+
req = make_request(method, url) # returns a RetriableResponseFuture
|
59
|
+
multi.do(req) # queues the request/future into hydra
|
60
|
+
end
|
61
|
+
|
62
|
+
multi.run # a blocking call, like hydra.run
|
63
|
+
|
64
|
+
```
|
65
|
+
`multi.context` encapsulates the block into a [`Fiber`](http://ruby-doc.org/core-2.2.0/Fiber.html) object and immediately runs (or `resume`, in Fiber's term) that fiber until the block explicitly gives up control. The method returns `multi` itself.
|
66
|
+
|
67
|
+
`multi.do(request_or_future,retries)` queues the request into `hydra`. It always returns a `RetriableResponseFuture`. A [`Typhoeus::Request`](https://github.com/typhoeus/typhoeus) will be converted into a `RetriableResponseFuture ` in this call.
|
68
|
+
|
69
|
+
Finally, `multi.run` starts `hydra` to execute the requests in parallel. The request is made as soon as the multiplexer is started. You get the results of the request by evaluating the value of the `RetriableResponseFuture`.
|
70
|
+
|
40
71
|
#### Px::Service::Client::Caching
|
41
72
|
|
73
|
+
Provides client-side response caching of service requests.
|
74
|
+
|
42
75
|
```ruby
|
43
76
|
include Px::Service::Client::Caching
|
44
77
|
|
@@ -52,36 +85,33 @@ caching do |config|
|
|
52
85
|
config.cache_client = Dalli::Client.new(...)
|
53
86
|
config.cache_logger = Logger.new(STDOUT) # or Rails.logger, for example
|
54
87
|
end
|
88
|
+
|
89
|
+
# An example of a cached request
|
90
|
+
result = cache_request(url, :last_resort, refresh_probability: 1) do
|
91
|
+
req = make_request(method, url)
|
92
|
+
end
|
55
93
|
```
|
56
94
|
|
57
|
-
|
95
|
+
`cache_request` expects a block that does the `make_request` and returns a `RetriableResponseFuture`. The block takes no argument. If neither the cache nor the response has the data, the exception `ServiceError` will be re-raised.
|
96
|
+
|
97
|
+
Responses are cached in memcached (using the provided cache client) in either a *last-resort* or *first-resort* manner.
|
58
98
|
|
59
99
|
*last-resort* means that the cached value is only used when the service client request fails (with a
|
60
|
-
`ServiceError`).
|
61
|
-
|
100
|
+
`ServiceError`). If the service client request succeeds, there is a chance that the cache value may get refreshed. The `refresh_probability` is provided to let the cached value
|
101
|
+
be refreshed probabilistically (rather than on every request).
|
62
102
|
|
63
|
-
|
64
|
-
when the cached value is close to expiry.
|
103
|
+
If the service client request fails and there is a `ServiceError`, `cache_logger` will record the exception message, and attempt to read the existing cache value.
|
65
104
|
|
66
|
-
|
105
|
+
*first-resort* means that the cached value is always used, if present. If the cached value is present but expired, the it sends the service client request and, if the request succeeds, it refreshes the cached value expiry. If the request fails, it uses the expired cached value, but the value remain expired. A retry may be needed.
|
67
106
|
|
68
|
-
```ruby
|
69
|
-
req = subject.make_request(method, url)
|
70
|
-
result = subject.cache_request(url) do
|
71
|
-
resp = nil
|
72
|
-
multi.context do
|
73
|
-
resp = multi.do(req)
|
74
|
-
end.run
|
75
|
-
|
76
|
-
resp
|
77
|
-
end
|
78
|
-
```
|
79
107
|
|
80
|
-
`cache_request` expects a block that returns a `RetriableResponseFuture`. It then returns a `Typhoeus::Response`.
|
81
108
|
|
82
109
|
#### Px::Service::Client::CircuitBreaker
|
110
|
+
This mixin overrides `Px::Service::Client::Base#make_request` method and implements the circuit breaker pattern.
|
83
111
|
|
84
112
|
```ruby
|
113
|
+
include Px::Service::Client::CircuitBreaker
|
114
|
+
|
85
115
|
# Optional
|
86
116
|
circuit_handler do |handler|
|
87
117
|
handler.logger = Logger.new(STDOUT)
|
@@ -90,25 +120,67 @@ circuit_handler do |handler|
|
|
90
120
|
handler.invocation_timeout = 10
|
91
121
|
handler.excluded_exceptions += [NotConsideredFailureException]
|
92
122
|
end
|
123
|
+
|
124
|
+
# An example of a make a request with circuit breaker
|
125
|
+
req = make_request(method, url) # overrides Px::Service::Client::Base
|
93
126
|
```
|
94
127
|
|
95
|
-
Adds a circuit breaker to the client.
|
128
|
+
Adds a circuit breaker to the client. `make_request` always returns `RetriableResponseFuture`
|
129
|
+
|
130
|
+
The circuit will open on any exception from the wrapped method, or if the request runs for longer than the `invocation_timeout`.
|
96
131
|
|
97
|
-
|
132
|
+
If the circuit is open, any future request will be get an error message wrapped in `Px::Service::ServiceError`.
|
133
|
+
|
134
|
+
By default, `Px::Service::ServiceRequestError` is excluded by the handler. That is, when the request fails with a `ServiceRequestError` exceptions, the same `ServiceRequestError` will be raised. But it does NOT increase the failure count or trip the breaker, as these exceptions indicate an error on the caller's part (e.g. an HTTP 4xx error).
|
98
135
|
|
99
136
|
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
|
100
|
-
`Px::Service::Client::Base
|
137
|
+
`Px::Service::Client::Base`.
|
101
138
|
|
102
139
|
This module is based on (and uses) the [Circuit Breaker](https://github.com/wsargent/circuit_breaker) gem by Will Sargent.
|
103
140
|
|
141
|
+
#### Px::Service::Client::HmacSigning
|
142
|
+
Similar to `Px::Service::Client::CircuitBreaker`, this mixin overrides `Px::Service::Client::Base#make_request` method and appends a HMAC signature in the request header.
|
143
|
+
|
144
|
+
To use this mixin:
|
145
|
+
|
146
|
+
```ruby
|
147
|
+
class MyClient < Px::Service::Client::Base
|
148
|
+
include Px::Service::Client::HmacSigning
|
149
|
+
|
150
|
+
#optional
|
151
|
+
hmac_signing do |config|
|
152
|
+
config.key = 'mykey'
|
153
|
+
config.keyspan = 300
|
154
|
+
end
|
155
|
+
end
|
156
|
+
```
|
157
|
+
|
158
|
+
Note: `key` and `keyspan` are class variables and shared among instances of the same class.
|
159
|
+
|
160
|
+
The signature is produced from the secret key, a nonce, HTTP method, url, query, body. The nonce is generated from the timestamp.
|
161
|
+
|
162
|
+
To retrieve and verify the signature:
|
163
|
+
|
164
|
+
```ruby
|
165
|
+
# Make a request with signed headers
|
166
|
+
resp = make_request(method, url, query, headers, body)
|
167
|
+
|
168
|
+
signature = resp.request.options[:headers]["X-Service-Auth"]
|
169
|
+
timestamp = resp.request.options[:headers]["Timestamp"]
|
170
|
+
|
171
|
+
# Call the class method to regenerate the signature
|
172
|
+
expected_signature = MyClient.generate_signature(method, url, query, body, timestamp)
|
173
|
+
|
174
|
+
# assert signature == expected_signature
|
175
|
+
```
|
176
|
+
|
104
177
|
#### Px::Service::Client::ListResponse
|
105
178
|
|
106
179
|
```ruby
|
107
180
|
def get_something(page, page_size)
|
108
181
|
response = JSON.parse(http_get("http://some/url?p=#{page}&l=#{page_size}"))
|
109
182
|
return Px::Service::Client::ListResponse(page_size, response, "items")
|
110
|
-
end
|
111
|
-
|
183
|
+
end
|
112
184
|
```
|
113
185
|
|
114
186
|
Wraps a deserialized response. A `ListResponse` implements the Ruby `Enumerable` module, as well
|
@@ -0,0 +1,67 @@
|
|
1
|
+
module Px::Service::Client
|
2
|
+
module HmacSigning
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
included do
|
5
|
+
alias_method_chain :make_request, :signing
|
6
|
+
|
7
|
+
cattr_accessor :secret do
|
8
|
+
DEFAULT_SECRET
|
9
|
+
end
|
10
|
+
|
11
|
+
cattr_accessor :keyspan do
|
12
|
+
DEFAULT_KEYSPAN
|
13
|
+
end
|
14
|
+
|
15
|
+
end
|
16
|
+
|
17
|
+
module ClassMethods
|
18
|
+
DefaultConfig = Struct.new(:secret, :keyspan) do
|
19
|
+
def initialize
|
20
|
+
self.secret = DEFAULT_SECRET
|
21
|
+
self.keyspan = DEFAULT_KEYSPAN
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
# initialize the config variables (including secret, keyspan) for hmac siging
|
26
|
+
def hmac_signing(&block)
|
27
|
+
@signing_config = DefaultConfig.new
|
28
|
+
|
29
|
+
# use default config if no block is given
|
30
|
+
if block_given?
|
31
|
+
yield(@signing_config)
|
32
|
+
self.secret = @signing_config.secret
|
33
|
+
self.keyspan = @signing_config.keyspan
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
##
|
38
|
+
# Generate a nonce that's used to expire message after keyspan seconds
|
39
|
+
def generate_signature(method, uri, query, body, timestamp)
|
40
|
+
secret = self.secret
|
41
|
+
keyspan = self.keyspan
|
42
|
+
nonce = (timestamp - (timestamp % keyspan)) + keyspan
|
43
|
+
data = "#{method.capitalize},#{uri},#{query},#{body},#{nonce.to_s}"
|
44
|
+
digest = OpenSSL::Digest.new('sha256')
|
45
|
+
digest = OpenSSL::HMAC.digest(digest, secret, data)
|
46
|
+
return Base64.urlsafe_encode64(digest).strip()
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def make_request_with_signing(method, uri, query: nil, headers: nil, body: nil)
|
51
|
+
timestamp = Time.now.to_i
|
52
|
+
signature = self.class.generate_signature(method, uri, query, body, timestamp)
|
53
|
+
|
54
|
+
headers = {} if headers.nil?
|
55
|
+
headers.merge!("X-Service-Auth" => signature)
|
56
|
+
headers.merge!("Timestamp" => timestamp)
|
57
|
+
|
58
|
+
make_request_without_signing(
|
59
|
+
method,
|
60
|
+
uri,
|
61
|
+
query: query,
|
62
|
+
headers: headers,
|
63
|
+
body: body)
|
64
|
+
end
|
65
|
+
|
66
|
+
end
|
67
|
+
end
|
data/lib/px/service/client.rb
CHANGED
@@ -6,6 +6,8 @@ require 'typhoeus'
|
|
6
6
|
module Px
|
7
7
|
module Service
|
8
8
|
module Client
|
9
|
+
DEFAULT_SECRET = "devsecret"
|
10
|
+
DEFAULT_KEYSPAN = 300
|
9
11
|
end
|
10
12
|
end
|
11
13
|
end
|
@@ -14,6 +16,7 @@ require "px/service/client/version"
|
|
14
16
|
require "px/service/client/future"
|
15
17
|
require "px/service/client/caching"
|
16
18
|
require "px/service/client/circuit_breaker"
|
19
|
+
require "px/service/client/hmac_signing"
|
17
20
|
require "px/service/client/list_response"
|
18
21
|
require "px/service/client/base"
|
19
22
|
require "px/service/client/multiplexer"
|
@@ -0,0 +1,45 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Px::Service::Client::HmacSigning do
|
4
|
+
let(:subject_class) {
|
5
|
+
Class.new(Px::Service::Client::Base).tap do |c|
|
6
|
+
# Anonymous classes don't have a name. Stub out :name so that things work
|
7
|
+
allow(c).to receive(:name).and_return("HmacSigning")
|
8
|
+
c.include(Px::Service::Client::HmacSigning)
|
9
|
+
end
|
10
|
+
}
|
11
|
+
|
12
|
+
let(:another_class) {
|
13
|
+
Class.new(Px::Service::Client::Base).include(Px::Service::Client::HmacSigning).tap do |c|
|
14
|
+
c.hmac_signing do |signing_config|
|
15
|
+
signing_config.secret = "different secret"
|
16
|
+
end
|
17
|
+
end
|
18
|
+
}
|
19
|
+
|
20
|
+
subject { subject_class.new }
|
21
|
+
let(:another_object) { another_class.new }
|
22
|
+
|
23
|
+
describe '#make_request' do
|
24
|
+
context "when the underlying request method succeeds" do
|
25
|
+
let(:url) { 'http://localhost:3000/path' }
|
26
|
+
let(:resp) { subject.send(:make_request, 'get', url) }
|
27
|
+
let(:headers) { resp.request.options[:headers] }
|
28
|
+
|
29
|
+
it "returns a Future" do
|
30
|
+
expect(resp).to be_a_kind_of(Px::Service::Client::RetriableResponseFuture)
|
31
|
+
end
|
32
|
+
|
33
|
+
it "contains a header with auth signature" do
|
34
|
+
expect(headers).to have_key("X-Service-Auth")
|
35
|
+
expect(headers).to have_key("Timestamp")
|
36
|
+
end
|
37
|
+
|
38
|
+
let(:resp2) { another_object.send(:make_request, 'get', url) }
|
39
|
+
let(:headers2) { resp2.request.options[:headers] }
|
40
|
+
it "is different from the object of another class with a different key" do
|
41
|
+
expect(headers["X-Service-Auth"]).not_to eq(headers2["X-Service-Auth"])
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
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.2.
|
4
|
+
version: 1.2.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Chris Micacchi
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2016-
|
11
|
+
date: 2016-03-31 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: will_paginate
|
@@ -246,6 +246,7 @@ files:
|
|
246
246
|
- lib/px/service/client/circuit_breaker.rb
|
247
247
|
- lib/px/service/client/circuit_breaker_retriable_response_future.rb
|
248
248
|
- lib/px/service/client/future.rb
|
249
|
+
- lib/px/service/client/hmac_signing.rb
|
249
250
|
- lib/px/service/client/list_response.rb
|
250
251
|
- lib/px/service/client/multiplexer.rb
|
251
252
|
- lib/px/service/client/retriable_response_future.rb
|
@@ -256,6 +257,7 @@ files:
|
|
256
257
|
- spec/px/service/client/caching/caching_spec.rb
|
257
258
|
- spec/px/service/client/circuit_breaker_spec.rb
|
258
259
|
- spec/px/service/client/future_spec.rb
|
260
|
+
- spec/px/service/client/hmac_signing_spec.rb
|
259
261
|
- spec/px/service/client/list_response_spec.rb
|
260
262
|
- spec/px/service/client/multiplexer_spec.rb
|
261
263
|
- spec/px/service/client/retriable_response_future_spec.rb
|
@@ -293,6 +295,7 @@ test_files:
|
|
293
295
|
- spec/px/service/client/caching/caching_spec.rb
|
294
296
|
- spec/px/service/client/circuit_breaker_spec.rb
|
295
297
|
- spec/px/service/client/future_spec.rb
|
298
|
+
- spec/px/service/client/hmac_signing_spec.rb
|
296
299
|
- spec/px/service/client/list_response_spec.rb
|
297
300
|
- spec/px/service/client/multiplexer_spec.rb
|
298
301
|
- spec/px/service/client/retriable_response_future_spec.rb
|