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: 2bcf837fdfb9024b8ba0da1bbb00919a7fef30f69b5145f93fa7d44cc8202989
4
- data.tar.gz: 16916a1f6be864f19999e16c808499a91df7a425ce7855e14fbc7a6ff2e3710f
3
+ metadata.gz: 72d21c18d06f3f3c99a95b4ebe7573b9fc61021579f707501a2f00652007df03
4
+ data.tar.gz: 131a14bfcc89c7a04414f044c0ad02aacc6dbf95920add865eb14ee4ecb26e78
5
5
  SHA512:
6
- metadata.gz: 2d7cf234140ce2e3f0ca7e3c6e4983e0a89d411067156da2f9c55334623de060c2abe2e8e26b1747b97b88ceb25fd8aeddcde98c156093f41f58032cb3eb3b2f
7
- data.tar.gz: cde80e613c12e4db455e0f08b8c253c74eec1b0a2f5947b1d57dfa76bc6ce0152632d24b5a97150f9702d493411fe5194d9fe604fb9633df98d28424685e8c34
6
+ metadata.gz: ceee786b3b488f7a3f0b5fcbe831ee5318cc017a19c2f2771252206ff8a3319dc6b2165f41f774f29b1ee32f50adcdc2e154436a4b640c294c12a1c1e3c84c1b
7
+ data.tar.gz: 567a6f507af84f76df13eb64fa333aeb51079fcd1efe2950bdda027ee8b3036b13b26fbba3fae8f104cff1e22ef56701dee25f773f96b0c1c957ad255a246524
@@ -94,6 +94,10 @@ module Startback
94
94
  status 428
95
95
  end
96
96
 
97
+ class TooManyRequestsError < BadRequestError
98
+ status 429
99
+ end
100
+
97
101
  class InternalServerError < Error
98
102
  status 500
99
103
  keep_error(true)
@@ -10,8 +10,14 @@ module Startback
10
10
  !!@rate_limit
11
11
  end
12
12
 
13
- def rate_limit_options(defaults)
14
- defaults.merge(@rate_limit || {})
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, # simply ignore the call
15
- # detection: :input, # method to call on Operation instance to detect call duplicates via pure data
16
- # periodicity: 60, # periodicity of occurence count, in seconds
17
- # max_occurence: 3, # max number of occurences during the period
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
- save_detection_count(key, count + 1, limit_options) if authorize
59
- [key, authorize]
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,
@@ -2,7 +2,7 @@ module Startback
2
2
  module Version
3
3
  MAJOR = 1
4
4
  MINOR = 2
5
- TINY = 0
5
+ TINY = 1
6
6
  end
7
7
  VERSION = "#{Version::MAJOR}.#{Version::MINOR}.#{Version::TINY}"
8
8
  end
@@ -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
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: startback
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
  - Bernard Lambeau