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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 3d1514cf2a39e3ae5f63e5f2d7dfeec339883184
4
- data.tar.gz: 7efc4b49b0511ed461e04bf65852617b63cac69e
3
+ metadata.gz: 7f2dd0390e5bbcf9ae63a8f2955d271b307b4d50
4
+ data.tar.gz: 66836a3d9ccb4189bc45edd01a94714d051eb620
5
5
  SHA512:
6
- metadata.gz: a0103f6e80ec442f822af2177d2cab2c38a0b816f19b4020375d641d4309947a381a9951900544a6d3008286d58d9c293904a43d04e1560390a9c32fad0b10a7
7
- data.tar.gz: c1f53ecb59e604b0d58e5ca166f8b826b7c78ba13889a09a4791fc6bb4ac12132f449d2305a77e1402f955e3b06a0e1821bf9b3ee768332feffa6deec3eaed46
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
@@ -2,7 +2,7 @@ require "prop/limiter"
2
2
  require "forwardable"
3
3
 
4
4
  module Prop
5
- VERSION = "2.0.4"
5
+ VERSION = "2.1.0"
6
6
 
7
7
  # Short hand for accessing Prop::Limiter methods
8
8
  class << self
@@ -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, 0, raw: true)
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) || default_bucket
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, default_bucket)
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 larger than :threshold")
56
+ raise ArgumentError.new(":burst_rate must be an Integer and not less than :threshold")
57
57
  end
58
- end
59
58
 
60
- private
59
+ if options[:first_throttled]
60
+ raise ArgumentError.new(":first_throttled is not supported")
61
+ end
62
+ end
61
63
 
62
- def default_bucket
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
- counter = @strategy.increment(cache_key, options)
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
- if throttle(handle, key, options)
103
- raise Prop::RateLimited.new(options.merge(cache_key: cache_key, handle: handle))
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 : @strategy.counter(cache_key, options)
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
@@ -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
- interval = options.fetch(:interval).to_i
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
- self.handle = handle
15
- self.cache_key = cache_key
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[@handle]
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
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: 2015-12-17 00:00:00.000000000 Z
11
+ date: 2016-01-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rake