prop 2.0.4 → 2.1.0
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 +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
|