prop 2.0.4 → 2.1.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 +27 -0
- data/lib/prop.rb +1 -1
- data/lib/prop/interval_strategy.rb +9 -1
- data/lib/prop/leaky_bucket_strategy.rb +8 -6
- data/lib/prop/limiter.rb +34 -19
- data/lib/prop/rate_limited.rb +9 -11
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7f2dd0390e5bbcf9ae63a8f2955d271b307b4d50
|
4
|
+
data.tar.gz: 66836a3d9ccb4189bc45edd01a94714d051eb620
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8eab2e9e05c2a3c7e9e7b4cc84c483f532eccfbf86559c10f44df4dc03f6f267a5054f55a9a82f1fe9fe22744841638520dcb98e3d31063e441c04c8cae06b7f
|
7
|
+
data.tar.gz: dbc80fce9d3466f41194c4595dd4e3847d82879225c0ae8c5bae0a3903dfc8345792b36ce8447a7f3cc31cdf54a8900ab222e5e1dc3f9bf302ab615da5e8bb7a
|
data/README.md
CHANGED
@@ -184,6 +184,33 @@ rescue Prop::RateLimited => e
|
|
184
184
|
end
|
185
185
|
```
|
186
186
|
|
187
|
+
## First throttled
|
188
|
+
|
189
|
+
You can opt to be notified when the throttle is breached for the first time.<br/>
|
190
|
+
This can be used to send notifications on breaches but prevent spam on multiple throttle breaches.
|
191
|
+
|
192
|
+
```Ruby
|
193
|
+
Prop.configure(:mails_per_hour, threshold: 100, interval: 1.hour, first_throttled: true)
|
194
|
+
|
195
|
+
throttled = Prop.throttle(:mails_per_hour, user.id, increment: 60)
|
196
|
+
if throttled
|
197
|
+
if throttled == :first_throttled
|
198
|
+
ApplicationMailer.spammer_warning(user).deliver_now
|
199
|
+
end
|
200
|
+
Rails.logger.warn("Not sending emails")
|
201
|
+
else
|
202
|
+
send_emails
|
203
|
+
end
|
204
|
+
|
205
|
+
# return values of throttle are: false, :first_throttled, true
|
206
|
+
|
207
|
+
Prop.first_throttled(:mails_per_hour, 1, increment: 60) # -> false
|
208
|
+
Prop.first_throttled(:mails_per_hour, 1, increment: 60) # -> :first_throttled
|
209
|
+
Prop.first_throttled(:mails_per_hour, 1, increment: 60) # -> true
|
210
|
+
|
211
|
+
# can also be accesses on `Prop::RateLimited` exceptions as `.first_throttled`
|
212
|
+
```
|
213
|
+
|
187
214
|
## Using Leaky Bucket Algorithm
|
188
215
|
|
189
216
|
You can add two additional configurations: `:strategy` and `:burst_rate` to use the
|
data/lib/prop.rb
CHANGED
@@ -5,6 +5,10 @@ require 'prop/key'
|
|
5
5
|
module Prop
|
6
6
|
class IntervalStrategy
|
7
7
|
class << self
|
8
|
+
def zero_counter
|
9
|
+
0
|
10
|
+
end
|
11
|
+
|
8
12
|
def counter(cache_key, options)
|
9
13
|
Prop::Limiter.cache.read(cache_key).to_i
|
10
14
|
end
|
@@ -17,7 +21,7 @@ module Prop
|
|
17
21
|
end
|
18
22
|
|
19
23
|
def reset(cache_key)
|
20
|
-
Prop::Limiter.cache.write(cache_key,
|
24
|
+
Prop::Limiter.cache.write(cache_key, zero_counter, raw: true)
|
21
25
|
end
|
22
26
|
|
23
27
|
def compare_threshold?(counter, operator, options)
|
@@ -25,6 +29,10 @@ module Prop
|
|
25
29
|
counter.send operator, options.fetch(:threshold)
|
26
30
|
end
|
27
31
|
|
32
|
+
def first_throttled?(counter, options)
|
33
|
+
(counter - options.fetch(:increment, 1)) <= options.fetch(:threshold)
|
34
|
+
end
|
35
|
+
|
28
36
|
# Builds the expiring cache key
|
29
37
|
def build(options)
|
30
38
|
key = options.fetch(:key)
|
@@ -7,7 +7,7 @@ module Prop
|
|
7
7
|
class LeakyBucketStrategy
|
8
8
|
class << self
|
9
9
|
def counter(cache_key, options)
|
10
|
-
bucket = Prop::Limiter.cache.read(cache_key) ||
|
10
|
+
bucket = Prop::Limiter.cache.read(cache_key) || zero_counter
|
11
11
|
now = Time.now.to_i
|
12
12
|
leak_amount = (now - bucket.fetch(:last_updated)) / options.fetch(:interval) * options.fetch(:threshold)
|
13
13
|
|
@@ -26,7 +26,7 @@ module Prop
|
|
26
26
|
end
|
27
27
|
|
28
28
|
def reset(cache_key)
|
29
|
-
Prop::Limiter.cache.write(cache_key,
|
29
|
+
Prop::Limiter.cache.write(cache_key, zero_counter)
|
30
30
|
end
|
31
31
|
|
32
32
|
def compare_threshold?(counter, operator, options)
|
@@ -53,13 +53,15 @@ module Prop
|
|
53
53
|
Prop::IntervalStrategy.validate_options!(options)
|
54
54
|
|
55
55
|
if !options[:burst_rate].is_a?(Fixnum) || options[:burst_rate] < options[:threshold]
|
56
|
-
raise ArgumentError.new(":burst_rate must be an Integer and
|
56
|
+
raise ArgumentError.new(":burst_rate must be an Integer and not less than :threshold")
|
57
57
|
end
|
58
|
-
end
|
59
58
|
|
60
|
-
|
59
|
+
if options[:first_throttled]
|
60
|
+
raise ArgumentError.new(":first_throttled is not supported")
|
61
|
+
end
|
62
|
+
end
|
61
63
|
|
62
|
-
def
|
64
|
+
def zero_counter
|
63
65
|
{ bucket: 0, last_updated: 0 }
|
64
66
|
end
|
65
67
|
end
|
data/lib/prop/limiter.rb
CHANGED
@@ -70,21 +70,9 @@ module Prop
|
|
70
70
|
# (optional) a block of code that this throttle is guarding
|
71
71
|
#
|
72
72
|
# Returns true if the threshold for this handle has been reached, else returns false
|
73
|
-
def throttle(handle, key = nil, options = {})
|
74
|
-
return false if disabled?
|
75
|
-
|
73
|
+
def throttle(handle, key = nil, options = {}, &block)
|
76
74
|
options, cache_key = prepare(handle, key, options)
|
77
|
-
|
78
|
-
|
79
|
-
if @strategy.compare_threshold?(counter, :>, options)
|
80
|
-
before_throttle_callback &&
|
81
|
-
before_throttle_callback.call(handle, key, options[:threshold], options[:interval])
|
82
|
-
|
83
|
-
true
|
84
|
-
else
|
85
|
-
yield if block_given?
|
86
|
-
false
|
87
|
-
end
|
75
|
+
_throttle(handle, key, cache_key, options, &block).first
|
88
76
|
end
|
89
77
|
|
90
78
|
# Public: Records a single action for the given handle/key combination.
|
@@ -96,14 +84,19 @@ module Prop
|
|
96
84
|
#
|
97
85
|
# Raises Prop::RateLimited if the threshold for this handle has been reached
|
98
86
|
# Returns the value of the block if given a such, otherwise the current count of the throttle
|
99
|
-
def throttle!(handle, key = nil, options = {})
|
87
|
+
def throttle!(handle, key = nil, options = {}, &block)
|
100
88
|
options, cache_key = prepare(handle, key, options)
|
101
|
-
|
102
|
-
|
103
|
-
|
89
|
+
throttled, counter = _throttle(handle, key, cache_key, options, &block)
|
90
|
+
|
91
|
+
if throttled
|
92
|
+
raise Prop::RateLimited.new(options.merge(
|
93
|
+
cache_key: cache_key,
|
94
|
+
handle: handle,
|
95
|
+
first_throttled: (throttled == :first_throttled)
|
96
|
+
))
|
104
97
|
end
|
105
98
|
|
106
|
-
block_given? ? yield :
|
99
|
+
block_given? ? yield : counter
|
107
100
|
end
|
108
101
|
|
109
102
|
# Public: Is the given handle/key combination currently throttled ?
|
@@ -148,6 +141,28 @@ module Prop
|
|
148
141
|
|
149
142
|
private
|
150
143
|
|
144
|
+
def _throttle(handle, key, cache_key, options)
|
145
|
+
return [false, @strategy.zero_counter] if disabled?
|
146
|
+
|
147
|
+
counter = @strategy.increment(cache_key, options)
|
148
|
+
|
149
|
+
if @strategy.compare_threshold?(counter, :>, options)
|
150
|
+
before_throttle_callback &&
|
151
|
+
before_throttle_callback.call(handle, key, options[:threshold], options[:interval])
|
152
|
+
|
153
|
+
result = if options[:first_throttled] && @strategy.first_throttled?(counter, options)
|
154
|
+
:first_throttled
|
155
|
+
else
|
156
|
+
true
|
157
|
+
end
|
158
|
+
|
159
|
+
[result, counter]
|
160
|
+
else
|
161
|
+
yield if block_given?
|
162
|
+
[false, counter]
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
151
166
|
def disabled?
|
152
167
|
!!@disabled
|
153
168
|
end
|
data/lib/prop/rate_limited.rb
CHANGED
@@ -1,23 +1,21 @@
|
|
1
1
|
module Prop
|
2
2
|
class RateLimited < StandardError
|
3
|
-
attr_accessor :handle, :cache_key, :retry_after, :description
|
3
|
+
attr_accessor :handle, :cache_key, :retry_after, :description, :first_throttled
|
4
4
|
|
5
5
|
def initialize(options)
|
6
|
-
handle = options.fetch(:handle)
|
7
|
-
cache_key = options.fetch(:cache_key)
|
8
|
-
|
9
|
-
strategy = options.fetch(:strategy)
|
10
|
-
|
11
|
-
super(strategy.threshold_reached(options))
|
12
|
-
|
6
|
+
self.handle = options.fetch(:handle)
|
7
|
+
self.cache_key = options.fetch(:cache_key)
|
8
|
+
self.first_throttled = options.fetch(:first_throttled)
|
13
9
|
self.description = options[:description]
|
14
|
-
|
15
|
-
|
10
|
+
|
11
|
+
interval = options.fetch(:interval).to_i
|
16
12
|
self.retry_after = interval - Time.now.to_i % interval
|
13
|
+
|
14
|
+
super(options.fetch(:strategy).threshold_reached(options))
|
17
15
|
end
|
18
16
|
|
19
17
|
def config
|
20
|
-
Prop.configurations
|
18
|
+
Prop.configurations.fetch(@handle)
|
21
19
|
end
|
22
20
|
end
|
23
21
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: prop
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.0
|
4
|
+
version: 2.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Morten Primdahl
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2016-01-22 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rake
|