startback 1.2.0 → 1.2.1
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
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 72d21c18d06f3f3c99a95b4ebe7573b9fc61021579f707501a2f00652007df03
|
4
|
+
data.tar.gz: 131a14bfcc89c7a04414f044c0ad02aacc6dbf95920add865eb14ee4ecb26e78
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ceee786b3b488f7a3f0b5fcbe831ee5318cc017a19c2f2771252206ff8a3319dc6b2165f41f774f29b1ee32f50adcdc2e154436a4b640c294c12a1c1e3c84c1b
|
7
|
+
data.tar.gz: 567a6f507af84f76df13eb64fa333aeb51079fcd1efe2950bdda027ee8b3036b13b26fbba3fae8f104cff1e22ef56701dee25f773f96b0c1c957ad255a246524
|
data/lib/startback/errors.rb
CHANGED
@@ -10,8 +10,14 @@ module Startback
|
|
10
10
|
!!@rate_limit
|
11
11
|
end
|
12
12
|
|
13
|
-
def rate_limit_options(defaults)
|
14
|
-
|
13
|
+
def rate_limit_options(op, defaults)
|
14
|
+
case @rate_limit
|
15
|
+
when NilClass then defaults
|
16
|
+
when Hash then defaults.merge(@rate_limit)
|
17
|
+
when Symbol then defaults.merge(op.send(@rate_limit))
|
18
|
+
else
|
19
|
+
raise ArgumentError
|
20
|
+
end
|
15
21
|
end
|
16
22
|
|
17
23
|
end # module RateLimit
|
@@ -11,16 +11,29 @@ module Startback
|
|
11
11
|
# RATE_LIMITER = Startback::Security::RateLimiter.new({
|
12
12
|
# store: Startback::Caching::Store.new, # use a redis cache store in practice
|
13
13
|
# defaults: {
|
14
|
-
# strategy: :silent_drop,
|
15
|
-
# detection: :input,
|
16
|
-
# periodicity: 60,
|
17
|
-
# max_occurence: 3,
|
14
|
+
# strategy: :silent_drop,
|
15
|
+
# detection: :input,
|
16
|
+
# periodicity: 60,
|
17
|
+
# max_occurence: 3,
|
18
18
|
# },
|
19
19
|
# })
|
20
20
|
#
|
21
21
|
# # in api.rb
|
22
22
|
# around_run(RATE_LIMITER)
|
23
23
|
#
|
24
|
+
# # in an operation.rb
|
25
|
+
# class Op < Startback::Operation
|
26
|
+
# rate_limit { max_occurences: 2 } # Partial<Config>
|
27
|
+
# rate_limit :dynamic_config # Config obtained on op instance
|
28
|
+
# end
|
29
|
+
#
|
30
|
+
# Reference:
|
31
|
+
#
|
32
|
+
# strategy: Symbol => :silent_drop or :fail (429 status code)
|
33
|
+
# detection: Symbol => method to call on Operation instance to detect call duplicates via pure data
|
34
|
+
# periodicity: Integer => periodicity of occurence count, in seconds
|
35
|
+
# max_occurences: Integer => max number of occurences during the period
|
36
|
+
#
|
24
37
|
class RateLimiter
|
25
38
|
include Support::Robustness
|
26
39
|
|
@@ -38,7 +51,7 @@ module Startback
|
|
38
51
|
raise ArgumentError, "A block is required" unless then_block
|
39
52
|
|
40
53
|
if op.class.has_rate_limit?
|
41
|
-
limit_options = op.class.rate_limit_options(defaults || {})
|
54
|
+
limit_options = op.class.rate_limit_options(op, defaults || {})
|
42
55
|
key, authorized = authorize_call!(op, limit_options)
|
43
56
|
unless authorized
|
44
57
|
log_rate_limited(op, key, limit_options)
|
@@ -54,9 +67,18 @@ module Startback
|
|
54
67
|
def authorize_call!(op, limit_options)
|
55
68
|
key = get_detection_key(op, limit_options)
|
56
69
|
count = get_detection_count(key)
|
70
|
+
strat = strategy(limit_options)
|
57
71
|
authorize = (count < max_occurences_allowed(limit_options))
|
58
|
-
|
59
|
-
|
72
|
+
if authorize
|
73
|
+
save_detection_count(key, count + 1, limit_options)
|
74
|
+
[key, authorize]
|
75
|
+
elsif strat === :silent_drop
|
76
|
+
[key, authorize]
|
77
|
+
elsif strat === :fail
|
78
|
+
raise Startback::Errors::TooManyRequestsError, op.class.name.to_s
|
79
|
+
else
|
80
|
+
[key, authorize]
|
81
|
+
end
|
60
82
|
end
|
61
83
|
|
62
84
|
def get_detection_count(key)
|
@@ -101,6 +123,10 @@ module Startback
|
|
101
123
|
limit_options[:max_occurences] || @options[:max_occurences] || 1
|
102
124
|
end
|
103
125
|
|
126
|
+
def strategy(limit_options)
|
127
|
+
limit_options[:strategy] || @options[:strategy] || :silent_drop
|
128
|
+
end
|
129
|
+
|
104
130
|
def log_rate_limited(op, key, limit_options)
|
105
131
|
logger_for(op).warn({
|
106
132
|
op: self.class,
|
data/lib/startback/version.rb
CHANGED
@@ -76,6 +76,31 @@ module Startback
|
|
76
76
|
end
|
77
77
|
end
|
78
78
|
|
79
|
+
context 'when :fail with a constant' do
|
80
|
+
class FailingRateLimitedOp < RateLimitedOp
|
81
|
+
rate_limit({
|
82
|
+
strategy: :fail,
|
83
|
+
detection: "constant"
|
84
|
+
})
|
85
|
+
end
|
86
|
+
|
87
|
+
let (:op_class) {
|
88
|
+
FailingRateLimitedOp
|
89
|
+
}
|
90
|
+
|
91
|
+
it 'works when called once' do
|
92
|
+
call_it_once
|
93
|
+
expect(op_class.called_count).to eql(1)
|
94
|
+
end
|
95
|
+
|
96
|
+
it 'fails on second call' do
|
97
|
+
call_it_once
|
98
|
+
expect {
|
99
|
+
call_it_once
|
100
|
+
}.to raise_error(Startback::Errors::TooManyRequestsError)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
79
104
|
context 'when silent_drop with a symbol' do
|
80
105
|
class InputRateLimitedOp < RateLimitedOp
|
81
106
|
rate_limit({
|
@@ -160,6 +185,31 @@ module Startback
|
|
160
185
|
expect(op_class.called_count).to eql(3)
|
161
186
|
end
|
162
187
|
end
|
188
|
+
|
189
|
+
context 'when using a dynamic configuration' do
|
190
|
+
class DynamicRateLimitedOp < RateLimitedOp
|
191
|
+
rate_limit :rate_limit_options
|
192
|
+
|
193
|
+
def rate_limit_options
|
194
|
+
{
|
195
|
+
strategy: :silent_drop,
|
196
|
+
detection: "constant",
|
197
|
+
max_occurences: input[:max],
|
198
|
+
}
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
let (:op_class) {
|
203
|
+
DynamicRateLimitedOp
|
204
|
+
}
|
205
|
+
|
206
|
+
it 'works when called three times in a row' do
|
207
|
+
call_it_once(max: 2)
|
208
|
+
call_it_once(max: 2)
|
209
|
+
call_it_once(max: 2)
|
210
|
+
expect(op_class.called_count).to eql(2)
|
211
|
+
end
|
212
|
+
end
|
163
213
|
end
|
164
214
|
end
|
165
215
|
end
|