lhc 12.1.1 → 12.3.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: a8c1cf6d1d9237c3cad231ae1fca7cadfcaaf6283eb429544945715235172b61
4
- data.tar.gz: 9945fabdbdd44fbef2fd472f0e4f7a37f347393b92fda1918abda13d6b65d195
3
+ metadata.gz: edceeb5ec4bd94dd580a56ed8a0ed0a71a31a8bd5c0b1f08dc96c7f8614d42b3
4
+ data.tar.gz: 183c118e2861b420877c37dfc8f86643d8fd410f8c2abbb8d681cdd168956108
5
5
  SHA512:
6
- metadata.gz: d208eca04aac94eb7f93b3053590df5c0626165cddf479ca3b1ea94907a71ee530df9de76f86f6b6492f14936a77d06f3ae296c9a854c8f1262866fdc4bad3bf
7
- data.tar.gz: eaa8166c752a295b34a2b6e0c3406bee09f771a77f2f17ba39b509396fa264ce9387ea5f2dc507fa50f51c2287c0d4fe57bc9849dfdada0a7fc2572aaf5527bf
6
+ metadata.gz: 92bd31139ebf0bc508e0681c99c5f35b8b9c93b0a6f56fdf2fcdbfb1faa1c3fa80c2894d48ebc31ad8c4138af5df9548b3f65f87a33bb5ebbf5a873a8d696585
7
+ data.tar.gz: 5b4f2fee1a99c4823ad1d0231624d6bf62794bff60bc27379c180b676439b88f89242813a2b306e44dde1946985114fa1190b1e67a22befdc089f41af520f759
data/README.md CHANGED
@@ -94,6 +94,7 @@ use it like:
94
94
 
95
95
 
96
96
 
97
+
97
98
  ## Basic methods
98
99
 
99
100
  Available are `get`, `post`, `put` & `delete`.
@@ -263,7 +264,7 @@ You can also use URL templates, when [configuring endpoints](#configuring-endpoi
263
264
  LHC.configure do |c|
264
265
  c.endpoint(:find_feedback, 'http://datastore/v2/feedbacks/{id}')
265
266
  end
266
-
267
+
267
268
  LHC.get(:find_feedback, params:{ id: 123 }) # GET http://datastore/v2/feedbacks/123
268
269
  ```
269
270
 
@@ -276,7 +277,7 @@ Working and configuring timeouts is important, to ensure your app stays alive wh
276
277
  LHC forwards two timeout options directly to typhoeus:
277
278
 
278
279
  `timeout` (in seconds) - The maximum time in seconds that you allow the libcurl transfer operation to take. Normally, name lookups can take a considerable time and limiting operations to less than a few seconds risk aborting perfectly normal operations. This option may cause libcurl to use the SIGALRM signal to timeout system calls.
279
- `connecttimeout` (in seconds) - It should contain the maximum time in seconds that you allow the connection phase to the server to take. This only limits the connection phase, it has no impact once it has connected. Set to zero to switch to the default built-in connection timeout - 300 seconds.
280
+ `connecttimeout` (in seconds) - It should contain the maximum time in seconds that you allow the connection phase to the server to take. This only limits the connection phase, it has no impact once it has connected. Set to zero to switch to the default built-in connection timeout - 300 seconds.
280
281
 
281
282
  ```ruby
282
283
  LHC.get('http://local.ch', timeout: 5, connecttimeout: 1)
@@ -481,7 +482,7 @@ You can configure global placeholders, that are used when generating urls from u
481
482
  c.placeholder(:datastore, 'http://datastore')
482
483
  c.endpoint(:feedbacks, '{+datastore}/feedbacks', { params: { has_reviews: true } })
483
484
  end
484
-
485
+
485
486
  LHC.get(:feedbacks) # http://datastore/v2/feedbacks
486
487
  ```
487
488
 
@@ -729,7 +730,7 @@ LHC::Monitoring.env = ENV['DEPLOYMENT_TYPE'] || Rails.env
729
730
 
730
731
  It tracks request attempts with `before_request` and `after_request` (counts).
731
732
 
732
- In case your workers/processes are getting killed due limited time constraints,
733
+ In case your workers/processes are getting killed due limited time constraints,
733
734
  you are able to detect deltas with relying on "before_request", and "after_request" counts:
734
735
 
735
736
  ```ruby
@@ -780,7 +781,7 @@ Logs basic request/response information to prometheus.
780
781
  LHC.configure do |c|
781
782
  c.interceptors = [LHC::Prometheus]
782
783
  end
783
-
784
+
784
785
  LHC::Prometheus.client = Prometheus::Client
785
786
  LHC::Prometheus.namespace = 'web_location_app'
786
787
  ```
@@ -802,7 +803,7 @@ If you enable the retry interceptor, you can have LHC retry requests for you:
802
803
  LHC.configure do |c|
803
804
  c.interceptors = [LHC::Retry]
804
805
  end
805
-
806
+
806
807
  response = LHC.get('http://local.ch', retry: true)
807
808
  ```
808
809
 
@@ -877,15 +878,15 @@ The throttle interceptor allows you to raise an exception if a predefined quota
877
878
  end
878
879
  ```
879
880
  ```ruby
880
- options = {
881
+ options = {
881
882
  throttle: {
882
- track: true, # enables tracking of current limit/remaining requests of rate-limiting
883
- break: '80%', # quota in percent after which errors are raised. Percentage symbol is optional, values will be converted to integer (e.g. '23.5' will become 23)
884
- provider: 'local.ch', # name of the provider under which throttling tracking is aggregated,
885
- limit: { header: 'Rate-Limit-Limit' }, # either a hard-coded integer, or a hash pointing at the response header containing the limit value
886
- remaining: { header: 'Rate-Limit-Remaining' }, # a hash pointing at the response header containing the current amount of remaining requests
887
- expires: { header: 'Rate-Limit-Reset' } # a hash pointing at the response header containing the timestamp when the quota will reset
888
- }
883
+ track: true,
884
+ break: '80%',
885
+ provider: 'local.ch',
886
+ limit: { header: 'Rate-Limit-Limit' },
887
+ remaining: { header: 'Rate-Limit-Remaining' },
888
+ expires: { header: 'Rate-Limit-Reset' }
889
+ }
889
890
  }
890
891
 
891
892
  LHC.get('http://local.ch', options)
@@ -895,6 +896,22 @@ LHC.get('http://local.ch', options)
895
896
  # raises LHC::Throttle::OutOfQuota: Reached predefined quota for local.ch
896
897
  ```
897
898
 
899
+ **Options Description**
900
+ * `track`: enables tracking of current limit/remaining requests of rate-limiting
901
+ * `break`: quota in percent after which errors are raised. Percentage symbol is optional, values will be converted to integer (e.g. '23.5' will become 23)
902
+ * `provider`: name of the provider under which throttling tracking is aggregated,
903
+ * `limit`:
904
+ * a hard-coded integer
905
+ * a hash pointing at the response header containing the limit value
906
+ * a proc that receives the response as argument and returns the limit value
907
+ * `remaining`:
908
+ * a hash pointing at the response header containing the current amount of remaining requests
909
+ * a proc that receives the response as argument and returns the current amount of remaining requests
910
+ * `expires`:
911
+ * a hash pointing at the response header containing the timestamp when the quota will reset
912
+ * a proc that receives the response as argument and returns the timestamp when the quota will reset
913
+
914
+
898
915
  #### Zipkin
899
916
 
900
917
  ** Zipkin 0.33 breaks our current implementation of the Zipkin interceptor **
@@ -3,7 +3,6 @@
3
3
  require 'active_support/duration'
4
4
 
5
5
  class LHC::Throttle < LHC::Interceptor
6
-
7
6
  class OutOfQuota < StandardError
8
7
  end
9
8
 
@@ -21,8 +20,7 @@ class LHC::Throttle < LHC::Interceptor
21
20
 
22
21
  def after_response
23
22
  options = response.request.options.dig(:throttle)
24
- return unless options
25
- return unless options.dig(:track)
23
+ return unless throttle?(options)
26
24
  self.class.track ||= {}
27
25
  self.class.track[options.dig(:provider)] = {
28
26
  limit: limit(options: options[:limit], response: response),
@@ -33,6 +31,10 @@ class LHC::Throttle < LHC::Interceptor
33
31
 
34
32
  private
35
33
 
34
+ def throttle?(options)
35
+ [options&.dig(:track), response.headers].none?(&:blank?)
36
+ end
37
+
36
38
  def break_when_quota_reached!
37
39
  options = request.options.dig(:throttle)
38
40
  track = (self.class.track || {}).dig(options[:provider])
@@ -46,36 +48,39 @@ class LHC::Throttle < LHC::Interceptor
46
48
  end
47
49
 
48
50
  def limit(options:, response:)
49
- @limit ||= begin
50
- if options.is_a?(Integer)
51
+ @limit ||=
52
+ if options.is_a?(Proc)
53
+ options.call(response)
54
+ elsif options.is_a?(Integer)
51
55
  options
52
- elsif options.is_a?(Hash) && options[:header] && response.headers.present?
56
+ elsif options.is_a?(Hash) && options[:header]
53
57
  response.headers[options[:header]]&.to_i
54
58
  end
55
- end
56
59
  end
57
60
 
58
61
  def remaining(options:, response:)
59
- @remaining ||= begin
60
- if options.is_a?(Hash) && options[:header] && response.headers.present?
61
- response.headers[options[:header]]&.to_i
62
+ @remaining ||=
63
+ begin
64
+ if options.is_a?(Proc)
65
+ options.call(response)
66
+ elsif options.is_a?(Hash) && options[:header]
67
+ response.headers[options[:header]]&.to_i
68
+ end
62
69
  end
63
- end
64
70
  end
65
71
 
66
72
  def expires(options:, response:)
67
- @expires ||= begin
68
- if options.is_a?(Hash) && options[:header] && response.headers.present?
69
- convert_expires(response.headers[options[:header]]&.to_i)
70
- else
71
- convert_expires(options)
72
- end
73
- end
73
+ @expires ||= convert_expires(read_expire_option(options, response))
74
+ end
75
+
76
+ def read_expire_option(options, response)
77
+ (options.is_a?(Hash) && options[:header]) ? response.headers[options[:header]] : options
74
78
  end
75
79
 
76
80
  def convert_expires(value)
77
- if value.is_a?(Integer)
78
- Time.zone.at(value).to_datetime
79
- end
81
+ return if value.blank?
82
+ return value.call(response) if value.is_a?(Proc)
83
+ return Time.parse(value) if value.match(/GMT/)
84
+ Time.zone.at(value.to_i).to_datetime
80
85
  end
81
86
  end
@@ -12,9 +12,9 @@ class LHC::Response::Data
12
12
  @data = data
13
13
 
14
14
  if as_json.is_a?(Hash)
15
- @base = LHC::Response::Data::Item.new(response, data: data)
15
+ @base = LHC::Response::Data::Item.new(@response, data: data)
16
16
  elsif as_json.is_a?(Array)
17
- @base = LHC::Response::Data::Collection.new(response, data: data)
17
+ @base = LHC::Response::Data::Collection.new(@response, data: data)
18
18
  end
19
19
  end
20
20
 
@@ -4,7 +4,7 @@
4
4
  # but made accssible in the ruby world
5
5
  module LHC::Response::Data::Base
6
6
  def as_json
7
- @json ||= (@data || response.format.as_json(response.body))
7
+ @json ||= (@data || @response.format.as_json(@response.body))
8
8
  end
9
9
 
10
10
  def as_open_struct
@@ -12,11 +12,7 @@ module LHC::Response::Data::Base
12
12
  if @data
13
13
  JSON.parse(@data.to_json, object_class: OpenStruct)
14
14
  else
15
- response.format.as_open_struct(response.body)
15
+ @response.format.as_open_struct(@response.body)
16
16
  end
17
17
  end
18
-
19
- private
20
-
21
- attr_reader :response
22
18
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module LHC
4
- VERSION ||= '12.1.1'
4
+ VERSION ||= '12.3.0'
5
5
  end
@@ -3,66 +3,64 @@
3
3
  require 'rails_helper'
4
4
 
5
5
  describe LHC::Throttle do
6
+ let(:options_break) { false }
7
+ let(:options_expires) { { header: 'reset' } }
8
+ let(:options_limit) { { header: 'limit' } }
9
+ let(:options_remaining) { { header: 'remaining' } }
6
10
  let(:provider) { 'local.ch' }
7
- let(:limit) { 10000 }
8
- let(:remaining) { 1900 }
11
+ let(:quota_limit) { 10_000 }
12
+ let(:quota_remaining) { 1900 }
13
+ let(:quota_reset) { (Time.zone.now + 1.hour).to_i }
9
14
  let(:options) do
10
15
  {
11
16
  throttle: {
12
17
  provider: provider,
13
18
  track: true,
14
- limit: limit_options,
15
- remaining: { header: 'Rate-Limit-Remaining' },
16
- expires: { header: 'Rate-Limit-Reset' },
17
- break: break_option
19
+ limit: options_limit,
20
+ remaining: options_remaining,
21
+ expires: options_expires,
22
+ break: options_break
18
23
  }
19
24
  }
20
25
  end
21
- let(:limit_options) { { header: 'Rate-Limit-Limit' } }
22
- let(:break_option) { false }
23
- let(:expires_in) { (Time.zone.now + 1.hour).to_i }
24
26
 
25
27
  before(:each) do
26
28
  LHC::Throttle.track = nil
27
29
  LHC.config.interceptors = [LHC::Throttle]
28
30
 
29
- stub_request(:get, 'http://local.ch')
30
- .to_return(
31
- headers: {
32
- 'Rate-Limit-Limit' => limit,
33
- 'Rate-Limit-Remaining' => remaining,
34
- 'Rate-Limit-Reset' => expires_in
35
- }
36
- )
31
+ stub_request(:get, 'http://local.ch').to_return(
32
+ headers: { 'limit' => quota_limit, 'remaining' => quota_remaining, 'reset' => quota_reset }
33
+ )
37
34
  end
38
35
 
39
36
  it 'tracks the request limits based on response data' do
40
37
  LHC.get('http://local.ch', options)
41
- expect(LHC::Throttle.track[provider][:limit]).to eq limit
42
- expect(LHC::Throttle.track[provider][:remaining]).to eq remaining
38
+ expect(LHC::Throttle.track[provider][:limit]).to eq quota_limit
39
+ expect(LHC::Throttle.track[provider][:remaining]).to eq quota_remaining
43
40
  end
44
41
 
45
42
  context 'fix predefined integer for limit' do
46
- let(:limit_options) { 1000 }
43
+ let(:options_limit) { 1000 }
47
44
 
48
45
  it 'tracks the limit based on initialy provided data' do
49
46
  LHC.get('http://local.ch', options)
50
- expect(LHC::Throttle.track[provider][:limit]).to eq limit_options
47
+ expect(LHC::Throttle.track[provider][:limit]).to eq options_limit
51
48
  end
52
49
  end
53
50
 
54
51
  context 'breaks' do
55
- let(:break_option) { '80%' }
52
+ let(:options_break) { '80%' }
56
53
 
57
54
  it 'hit the breaks if throttling quota is reached' do
58
55
  LHC.get('http://local.ch', options)
59
- expect(-> {
60
- LHC.get('http://local.ch', options)
61
- }).to raise_error(LHC::Throttle::OutOfQuota, 'Reached predefined quota for local.ch')
56
+ expect { LHC.get('http://local.ch', options) }.to raise_error(
57
+ LHC::Throttle::OutOfQuota,
58
+ 'Reached predefined quota for local.ch'
59
+ )
62
60
  end
63
61
 
64
62
  context 'still within quota' do
65
- let(:break_option) { '90%' }
63
+ let(:options_break) { '90%' }
66
64
 
67
65
  it 'does not hit the breaks' do
68
66
  LHC.get('http://local.ch', options)
@@ -72,17 +70,14 @@ describe LHC::Throttle do
72
70
  end
73
71
 
74
72
  context 'no response headers' do
75
- before do
76
- stub_request(:get, 'http://local.ch')
77
- .to_return(status: 200)
78
- end
73
+ before { stub_request(:get, 'http://local.ch').to_return(status: 200) }
79
74
 
80
75
  it 'does not raise an exception' do
81
76
  LHC.get('http://local.ch', options)
82
77
  end
83
78
 
84
79
  context 'no remaining tracked, but break enabled' do
85
- let(:break_option) { '90%' }
80
+ let(:options_break) { '90%' }
86
81
 
87
82
  it 'does not fail if a remaining was not tracked yet' do
88
83
  LHC.get('http://local.ch', options)
@@ -92,15 +87,150 @@ describe LHC::Throttle do
92
87
  end
93
88
 
94
89
  context 'expires' do
95
- let(:break_option) { '80%' }
90
+ let(:options_break) { '80%' }
96
91
 
97
92
  it 'attempts another request if the quota expired' do
98
93
  LHC.get('http://local.ch', options)
99
- expect(-> {
100
- LHC.get('http://local.ch', options)
101
- }).to raise_error(LHC::Throttle::OutOfQuota, 'Reached predefined quota for local.ch')
94
+ expect { LHC.get('http://local.ch', options) }.to raise_error(
95
+ LHC::Throttle::OutOfQuota,
96
+ 'Reached predefined quota for local.ch'
97
+ )
102
98
  Timecop.travel(Time.zone.now + 2.hours)
103
99
  LHC.get('http://local.ch', options)
104
100
  end
105
101
  end
102
+
103
+ describe 'configuration values as Procs' do
104
+ describe 'calculate "limit" in proc' do
105
+ let(:options_limit) do
106
+ ->(*) { 10_000 }
107
+ end
108
+
109
+ before(:each) do
110
+ LHC.get('http://local.ch', options)
111
+ end
112
+
113
+ context 'breaks' do
114
+ let(:options_break) { '80%' }
115
+
116
+ it 'hit the breaks if throttling quota is reached' do
117
+ expect { LHC.get('http://local.ch', options) }.to raise_error(
118
+ LHC::Throttle::OutOfQuota,
119
+ 'Reached predefined quota for local.ch'
120
+ )
121
+ end
122
+
123
+ context 'still within quota' do
124
+ let(:options_break) { '90%' }
125
+
126
+ it 'does not hit the breaks' do
127
+ LHC.get('http://local.ch', options)
128
+ end
129
+ end
130
+ end
131
+ end
132
+
133
+ describe 'calculate "remaining" in proc' do
134
+ let(:quota_current) { 8100 }
135
+ let(:options_remaining) do
136
+ ->(response) { (response.headers['limit']).to_i - (response.headers['current']).to_i }
137
+ end
138
+
139
+ before(:each) do
140
+ stub_request(:get, 'http://local.ch').to_return(
141
+ headers: { 'limit' => quota_limit, 'current' => quota_current, 'reset' => quota_reset }
142
+ )
143
+ LHC.get('http://local.ch', options)
144
+ end
145
+
146
+ context 'breaks' do
147
+ let(:options_break) { '80%' }
148
+
149
+ it 'hit the breaks if throttling quota is reached' do
150
+ expect { LHC.get('http://local.ch', options) }.to raise_error(
151
+ LHC::Throttle::OutOfQuota,
152
+ 'Reached predefined quota for local.ch'
153
+ )
154
+ end
155
+
156
+ context 'still within quota' do
157
+ let(:options_break) { '90%' }
158
+
159
+ it 'does not hit the breaks' do
160
+ LHC.get('http://local.ch', options)
161
+ end
162
+ end
163
+ end
164
+ end
165
+
166
+ describe 'calculate "reset" in proc' do
167
+ let(:options_expires) { ->(*) { Time.zone.now + 1.second } }
168
+
169
+ before(:each) do
170
+ stub_request(:get, 'http://local.ch').to_return(
171
+ headers: { 'limit' => quota_limit, 'remaining' => quota_remaining }
172
+ )
173
+ LHC.get('http://local.ch', options)
174
+ end
175
+
176
+ context 'breaks' do
177
+ let(:options_break) { '80%' }
178
+
179
+ it 'hit the breaks if throttling quota is reached' do
180
+ expect { LHC.get('http://local.ch', options) }.to raise_error(
181
+ LHC::Throttle::OutOfQuota,
182
+ 'Reached predefined quota for local.ch'
183
+ )
184
+ end
185
+
186
+ context 'still within quota' do
187
+ let(:options_break) { '90%' }
188
+
189
+ it 'does not hit the breaks' do
190
+ LHC.get('http://local.ch', options)
191
+ end
192
+ end
193
+ end
194
+ end
195
+ end
196
+
197
+ describe 'parsing reset time given in prose' do
198
+ let(:quota_reset) { (Time.zone.now + 1.day).strftime('%A, %B %d, %Y 12:00:00 AM GMT').to_s }
199
+
200
+ before { LHC.get('http://local.ch', options) }
201
+
202
+ context 'breaks' do
203
+ let(:options_break) { '80%' }
204
+
205
+ it 'hit the breaks if throttling quota is reached' do
206
+ expect { LHC.get('http://local.ch', options) }.to raise_error(
207
+ LHC::Throttle::OutOfQuota,
208
+ 'Reached predefined quota for local.ch'
209
+ )
210
+ end
211
+
212
+ context 'still within quota' do
213
+ let(:options_break) { '90%' }
214
+
215
+ it 'does not hit the breaks' do
216
+ LHC.get('http://local.ch', options)
217
+ end
218
+ end
219
+ end
220
+ end
221
+
222
+ context 'when value is empty' do
223
+ let(:quota_reset) { nil }
224
+
225
+ before do
226
+ stub_request(:get, 'http://local.ch').to_return(
227
+ headers: { 'limit' => quota_limit, 'remaining' => quota_remaining }
228
+ )
229
+ LHC.get('http://local.ch', options)
230
+ end
231
+
232
+ it 'still runs' do
233
+ LHC.get('http://local.ch', options)
234
+ end
235
+ end
106
236
  end
@@ -58,4 +58,28 @@ describe LHC::Response do
58
58
  end
59
59
  end
60
60
  end
61
+
62
+ context 'response data if responding error data contains a response' do
63
+ before do
64
+ stub_request(:get, "http://listings/")
65
+ .to_return(status: 404, body: {
66
+ meta: {
67
+ errors: [
68
+ { code: 2000, msg: 'I like to hide error messages (this is meta).' }
69
+ ]
70
+ },
71
+ response: 'why not?'
72
+ }.to_json)
73
+ end
74
+
75
+ it 'does not throw a stack level to deep issue when accessing data in a rescue context' do
76
+ begin
77
+ LHC.get('http://listings')
78
+ rescue LHC::Error => error
79
+ expect(
80
+ error.response.request.response.data.meta.errors.detect { |item| item.code == 2000 }.msg
81
+ ).to eq 'I like to hide error messages (this is meta).'
82
+ end
83
+ end
84
+ end
61
85
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lhc
3
3
  version: !ruby/object:Gem::Version
4
- version: 12.1.1
4
+ version: 12.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - https://github.com/local-ch/lhc/contributors
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-07-30 00:00:00.000000000 Z
11
+ date: 2020-08-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -389,7 +389,7 @@ homepage: https://github.com/local-ch/lhc
389
389
  licenses:
390
390
  - GPL-3.0
391
391
  metadata: {}
392
- post_install_message:
392
+ post_install_message:
393
393
  rdoc_options: []
394
394
  require_paths:
395
395
  - lib
@@ -405,8 +405,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
405
405
  version: '0'
406
406
  requirements:
407
407
  - Ruby >= 2.0.0
408
- rubygems_version: 3.0.6
409
- signing_key:
408
+ rubygems_version: 3.0.3
409
+ signing_key:
410
410
  specification_version: 4
411
411
  summary: Advanced HTTP Client for Ruby, fueled with interceptors
412
412
  test_files: