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 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