lhc 12.1.3 → 12.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +25 -14
- data/lib/lhc/interceptors/throttle.rb +26 -22
- data/lib/lhc/version.rb +1 -1
- data/spec/interceptors/throttle/main_spec.rb +91 -35
- metadata +6 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1a536d04e90db04fdeca6edd06c3e88782106a660d410a1b908b94147a3e9154
|
4
|
+
data.tar.gz: d0d89904a00069fdd8f3ef297f296e5da9d1383a4b4239aeebf00b6869be082a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 858291c8b53ce6447df1cc23fabd7c74ca4307ff7590eaebb76f4d538b769f81d9d89055bf79bf564814cf55c6832fccee42918100433061331252c6aafd8042
|
7
|
+
data.tar.gz: 49356178736094b85a4d9c1eab8dcf390024df5b580cb696e7f02683cbc1f1cf4a3e5bcfc6e03f5e8b9330fed1e02b94dd8bbbe17ec3c58e14b91b7352df9030
|
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,
|
883
|
-
break: '80%',
|
884
|
-
provider: 'local.ch',
|
885
|
-
limit: { header: 'Rate-Limit-Limit' },
|
886
|
-
remaining: { header: 'Rate-Limit-Remaining' },
|
887
|
-
expires: { header: 'Rate-Limit-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)
|
@@ -894,6 +895,16 @@ LHC.get('http://local.ch', options)
|
|
894
895
|
LHC.get('http://local.ch', options)
|
895
896
|
# raises LHC::Throttle::OutOfQuota: Reached predefined quota for local.ch
|
896
897
|
```
|
898
|
+
**Options Description**
|
899
|
+
* `track`: enables tracking of current limit/remaining requests of rate-limiting
|
900
|
+
* `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)
|
901
|
+
* `provider`: name of the provider under which throttling tracking is aggregated,
|
902
|
+
* `limit`: either a hard-coded integer, or a hash pointing at the response header containing the limit value
|
903
|
+
* `remaining`:
|
904
|
+
* a hash pointing at the response header containing the current amount of remaining requests
|
905
|
+
* a proc that receives the response as argument and returns the current amount
|
906
|
+
of remaining requests
|
907
|
+
* `expires`: a hash pointing at the response header containing the timestamp when the quota will reset
|
897
908
|
|
898
909
|
#### Zipkin
|
899
910
|
|
@@ -21,8 +21,7 @@ class LHC::Throttle < LHC::Interceptor
|
|
21
21
|
|
22
22
|
def after_response
|
23
23
|
options = response.request.options.dig(:throttle)
|
24
|
-
return unless options
|
25
|
-
return unless options.dig(:track)
|
24
|
+
return unless throttle?(options)
|
26
25
|
self.class.track ||= {}
|
27
26
|
self.class.track[options.dig(:provider)] = {
|
28
27
|
limit: limit(options: options[:limit], response: response),
|
@@ -33,6 +32,10 @@ class LHC::Throttle < LHC::Interceptor
|
|
33
32
|
|
34
33
|
private
|
35
34
|
|
35
|
+
def throttle?(options)
|
36
|
+
[options&.dig(:track), response.headers].none?(&:blank?)
|
37
|
+
end
|
38
|
+
|
36
39
|
def break_when_quota_reached!
|
37
40
|
options = request.options.dig(:throttle)
|
38
41
|
track = (self.class.track || {}).dig(options[:provider])
|
@@ -46,36 +49,37 @@ class LHC::Throttle < LHC::Interceptor
|
|
46
49
|
end
|
47
50
|
|
48
51
|
def limit(options:, response:)
|
49
|
-
@limit ||=
|
50
|
-
|
51
|
-
options
|
52
|
-
|
53
|
-
|
52
|
+
@limit ||=
|
53
|
+
begin
|
54
|
+
if options.is_a?(Integer)
|
55
|
+
options
|
56
|
+
elsif options.is_a?(Hash) && options[:header]
|
57
|
+
response.headers[options[:header]]&.to_i
|
58
|
+
end
|
54
59
|
end
|
55
|
-
end
|
56
60
|
end
|
57
61
|
|
58
62
|
def remaining(options:, response:)
|
59
|
-
@remaining ||=
|
60
|
-
|
61
|
-
|
63
|
+
@remaining ||=
|
64
|
+
begin
|
65
|
+
if options.is_a?(Proc)
|
66
|
+
options.call(response)
|
67
|
+
elsif options.is_a?(Hash) && options[:header]
|
68
|
+
response.headers[options[:header]]&.to_i
|
69
|
+
end
|
62
70
|
end
|
63
|
-
end
|
64
71
|
end
|
65
72
|
|
66
73
|
def expires(options:, response:)
|
67
|
-
@expires ||=
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
end
|
73
|
-
end
|
74
|
+
@expires ||= convert_expires(read_expire_option(options, response))
|
75
|
+
end
|
76
|
+
|
77
|
+
def read_expire_option(options, response)
|
78
|
+
(options.is_a?(Hash) && options[:header]) ? response.headers[options[:header]] : options
|
74
79
|
end
|
75
80
|
|
76
81
|
def convert_expires(value)
|
77
|
-
if value.
|
78
|
-
|
79
|
-
end
|
82
|
+
return Time.parse(value) if value.match(/GMT/)
|
83
|
+
Time.zone.at(value.to_i).to_datetime if value.present?
|
80
84
|
end
|
81
85
|
end
|
data/lib/lhc/version.rb
CHANGED
@@ -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(:
|
8
|
-
let(:
|
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:
|
15
|
-
remaining:
|
16
|
-
expires:
|
17
|
-
break:
|
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
|
-
|
31
|
-
|
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
|
42
|
-
expect(LHC::Throttle.track[provider][:remaining]).to eq
|
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(:
|
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
|
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(:
|
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
|
61
|
-
|
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(:
|
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
|
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(:
|
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,76 @@ describe LHC::Throttle do
|
|
92
87
|
end
|
93
88
|
|
94
89
|
context 'expires' do
|
95
|
-
let(:
|
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
|
101
|
-
|
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 'calculate "remaining" in proc' do
|
104
|
+
let(:quota_current) { 8100 }
|
105
|
+
let(:options_remaining) do
|
106
|
+
->(response) { (response.headers['limit']).to_i - (response.headers['current']).to_i }
|
107
|
+
end
|
108
|
+
|
109
|
+
before(:each) do
|
110
|
+
stub_request(:get, 'http://local.ch').to_return(
|
111
|
+
headers: { 'limit' => quota_limit, 'current' => quota_current, 'reset' => quota_reset }
|
112
|
+
)
|
113
|
+
LHC.get('http://local.ch', options)
|
114
|
+
end
|
115
|
+
|
116
|
+
context 'breaks' do
|
117
|
+
let(:options_break) { '80%' }
|
118
|
+
|
119
|
+
it 'hit the breaks if throttling quota is reached' do
|
120
|
+
expect { LHC.get('http://local.ch', options) }.to raise_error(
|
121
|
+
LHC::Throttle::OutOfQuota,
|
122
|
+
'Reached predefined quota for local.ch'
|
123
|
+
)
|
124
|
+
end
|
125
|
+
|
126
|
+
context 'still within quota' do
|
127
|
+
let(:options_break) { '90%' }
|
128
|
+
|
129
|
+
it 'does not hit the breaks' do
|
130
|
+
LHC.get('http://local.ch', options)
|
131
|
+
LHC.get('http://local.ch', options)
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
describe 'parsing reset time given in prose' do
|
138
|
+
let(:quota_reset) { (Time.zone.now + 1.day).strftime('%A, %B %d, %Y 12:00:00 AM GMT').to_s }
|
139
|
+
|
140
|
+
before { LHC.get('http://local.ch', options) }
|
141
|
+
|
142
|
+
context 'breaks' do
|
143
|
+
let(:options_break) { '80%' }
|
144
|
+
|
145
|
+
it 'hit the breaks if throttling quota is reached' do
|
146
|
+
expect { LHC.get('http://local.ch', options) }.to raise_error(
|
147
|
+
LHC::Throttle::OutOfQuota,
|
148
|
+
'Reached predefined quota for local.ch'
|
149
|
+
)
|
150
|
+
end
|
151
|
+
|
152
|
+
context 'still within quota' do
|
153
|
+
let(:options_break) { '90%' }
|
154
|
+
|
155
|
+
it 'does not hit the breaks' do
|
156
|
+
LHC.get('http://local.ch', options)
|
157
|
+
LHC.get('http://local.ch', options)
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
106
162
|
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.
|
4
|
+
version: 12.2.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-08-
|
11
|
+
date: 2020-08-24 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.
|
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:
|