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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 260462f7c8c782f9a5a1b7a0996431e426f01f39
4
- data.tar.gz: 31ffc0fff0138c3d8519d523da451b23e765de0c
3
+ metadata.gz: e91b9e1a396db568b3b30155336363538b80fff6
4
+ data.tar.gz: 72792c99dc994aeed39b1e0a0b586c486bc96e03
5
5
  SHA512:
6
- metadata.gz: dc00a6aca11761cc581dc3175a322027e8ee5ac8315933a6c72803665efae80bb5794ba5a25a83c0e831d5b01708f469cd84c79cd3cce8262ab71a5885a4e84f
7
- data.tar.gz: 6aefddddf56c6968920130e3aaacddc3bcdf0ef41677d747e651a00cb8afcfe561e4a0341cbcfedf90df0b68cdb0ff848442991b63fc3c9fac66d09a01f87941
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
- Provides client-side response caching of service requests. Responses are cached in memcached (using the provided cache client) in either a *last-resort* or *first-resort* manner.
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`). When using last-resort caching, a `refresh_probability` can be provided that causes the cached value
61
- to be refreshed probabilistically (rather than on every request).
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
- *first-resort* means that the cached value is always used, if present. Requests to the service are only made
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
- An example of a cached request:
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. The circuit will open on any exception from the wrapped method, or if the request runs for longer than the `invocation_timeout`.
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
- 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).
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
@@ -1,7 +1,7 @@
1
1
  module Px::Service::Client
2
2
  class Base
3
3
  cattr_accessor :logger
4
-
4
+
5
5
  private
6
6
 
7
7
  def parsed_body(response)
@@ -37,5 +37,6 @@ module Px::Service::Client
37
37
 
38
38
  RetriableResponseFuture.new(req)
39
39
  end
40
+
40
41
  end
41
42
  end
@@ -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
@@ -1,7 +1,7 @@
1
1
  module Px
2
2
  module Service
3
3
  module Client
4
- VERSION = "1.2.0"
4
+ VERSION = "1.2.1"
5
5
  end
6
6
  end
7
7
  end
@@ -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.0
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-02-26 00:00:00.000000000 Z
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