prop 1.1.0 → 1.2.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 +24 -10
- data/lib/prop.rb +1 -1
- data/lib/prop/interval_strategy.rb +59 -0
- data/lib/prop/key.rb +0 -12
- data/lib/prop/leaky_bucket_strategy.rb +68 -0
- data/lib/prop/limiter.rb +14 -13
- data/lib/prop/options.rb +13 -6
- data/lib/prop/rate_limited.rb +2 -2
- metadata +9 -27
- data/.document +0 -5
- data/.gemtest +0 -0
- data/.gitignore +0 -22
- data/.travis.yml +0 -6
- data/Gemfile +0 -3
- data/Rakefile +0 -12
- data/prop.gemspec +0 -26
- data/test/helper.rb +0 -11
- data/test/test_key.rb +0 -24
- data/test/test_limiter.rb +0 -153
- data/test/test_middleware.rb +0 -46
- data/test/test_options.rb +0 -44
- data/test/test_prop.rb +0 -205
- data/test/test_rate_limited.rb +0 -39
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2601b63983949b53ff9f31664203316614466b74
|
4
|
+
data.tar.gz: dc5deeebea017087d28a378e83783e3d7a5001ae
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: fcd418b8e8bd9c96bb84ad902db628ce7c792a3ff3324b52c682ed989ad951595aa35564f26386f742ee07f73c3af42b490cc1731614ae255b4cc2d2988608eb
|
7
|
+
data.tar.gz: 43d95cc4198eacf0c3fe20182788085c778a7d20d67ffeb97c1875f3f8488275d2ea2e0aa8e997e82ffffb2aab56a7c37bbab619fb8ca930d7b3c72b94a76150
|
data/README.md
CHANGED
@@ -1,9 +1,12 @@
|
|
1
1
|
|
2
|
-
# Prop [![Build Status](https://
|
2
|
+
# Prop [![Build Status](https://travis-ci.org/zendesk/prop.png)](https://travis-ci.org/zendesk/prop)
|
3
3
|
|
4
4
|
Prop is a simple gem for rate limiting requests of any kind. It allows you to configure hooks for registering certain actions, such that you can define thresholds, register usage and finally act on exceptions once thresholds get exceeded.
|
5
5
|
|
6
|
-
Prop
|
6
|
+
Prop supports two limiting strategies:
|
7
|
+
|
8
|
+
* Basic strategy (default): Prop will use an interval to define a window of time using simple div arithmetic. This means that it's a worst-case throttle that will allow up to two times the specified requests within the specified interval.
|
9
|
+
* Leaky bucket strategy: Prop also supports the [Leaky Bucket](https://en.wikipedia.org/wiki/Leaky_bucket) algorithm, which is similar to the basic strategy but also supports bursts up to a specified threshold.
|
7
10
|
|
8
11
|
To get going with Prop, you first define the read and write operations. These define how you write a registered request and how to read the number of requests for a given action. For example, do something like the below in a Rails initializer:
|
9
12
|
|
@@ -34,7 +37,7 @@ end
|
|
34
37
|
Once the read and write operations are defined, you can optionally define thresholds. If, for example, you want to have a threshold on accepted emails per hour from a given user, you could define a threshold and interval (in seconds) for this like so:
|
35
38
|
|
36
39
|
```ruby
|
37
|
-
Prop.configure(:mails_per_hour, :
|
40
|
+
Prop.configure(:mails_per_hour, threshold: 100, interval: 1.hour, description: "Mail rate limit exceeded")
|
38
41
|
```
|
39
42
|
|
40
43
|
The `:mails_per_hour` in the above is called the "handle". You can now put the throttle to work with these values, by passing the handle to the respective methods in Prop:
|
@@ -82,7 +85,7 @@ If the throttle! method gets called more than "threshold" times within "interval
|
|
82
85
|
```ruby
|
83
86
|
rescue_from Prop::RateLimited do |e|
|
84
87
|
if e.handle == :authorization_attempt
|
85
|
-
render :
|
88
|
+
render status: :forbidden, message: I18n.t(e.description)
|
86
89
|
elsif ...
|
87
90
|
|
88
91
|
end
|
@@ -142,8 +145,8 @@ Prop.throttle!(:mails_per_hour, nil)
|
|
142
145
|
The default (and smallest possible) increment is 1, you can set that to any integer value using :increment which is handy for building time based throttles:
|
143
146
|
|
144
147
|
```ruby
|
145
|
-
Prop.configure(:execute_time, :
|
146
|
-
Prop.throttle!(:execute_time, account.id, :
|
148
|
+
Prop.configure(:execute_time, threshold: 10, interval: 1.minute)
|
149
|
+
Prop.throttle!(:execute_time, account.id, increment: (Benchmark.realtime { execute }).to_i)
|
147
150
|
```
|
148
151
|
|
149
152
|
## Optional configuration
|
@@ -151,9 +154,9 @@ Prop.throttle!(:execute_time, account.id, :increment => (Benchmark.realtime { ex
|
|
151
154
|
You can add optional configuration to a prop and retrieve it using `Prop.configurations[:foo]`:
|
152
155
|
|
153
156
|
```ruby
|
154
|
-
Prop.configure(:api_query, :
|
155
|
-
Prop.configure(:api_insert, :
|
156
|
-
Prop.configure(:password_failure, :
|
157
|
+
Prop.configure(:api_query, threshold: 10, interval: 1.minute, category: :api)
|
158
|
+
Prop.configure(:api_insert, threshold: 50, interval: 1.minute, category: :api)
|
159
|
+
Prop.configure(:password_failure, threshold: 5, interval: 1.minute, category: :auth)
|
157
160
|
```
|
158
161
|
|
159
162
|
```
|
@@ -173,9 +176,20 @@ rescue Prop::RateLimited => e
|
|
173
176
|
end
|
174
177
|
```
|
175
178
|
|
179
|
+
## Using Leaky Bucket Algorithm
|
180
|
+
|
181
|
+
You can add two additional configurations: `:strategy` and `:burst_rate` to use the [leaky bucket algorithm](https://en.wikipedia.org/wiki/Leaky_bucket). Prop will handle the details after configured, and you don't have to specify `:strategy` again when using `throttle`, `throttle!` or any other methods.
|
182
|
+
|
183
|
+
```ruby
|
184
|
+
Prop.configure(:api_request, strategy: :leaky_bucket, burst_rate: 20, threshold: 5, interval: 1.minute)
|
185
|
+
```
|
186
|
+
|
187
|
+
* `:threshold` value here would be the "leak rate" of leaky bucket algorithm.
|
188
|
+
|
189
|
+
|
176
190
|
## License
|
177
191
|
|
178
|
-
Copyright
|
192
|
+
Copyright 2015 Zendesk
|
179
193
|
|
180
194
|
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License.
|
181
195
|
You may obtain a copy of the License at
|
data/lib/prop.rb
CHANGED
@@ -0,0 +1,59 @@
|
|
1
|
+
require 'prop/limiter'
|
2
|
+
require 'prop/options'
|
3
|
+
require 'prop/key'
|
4
|
+
|
5
|
+
module Prop
|
6
|
+
class IntervalStrategy
|
7
|
+
class << self
|
8
|
+
def counter(cache_key, options)
|
9
|
+
Prop::Limiter.reader.call(cache_key).to_i
|
10
|
+
end
|
11
|
+
|
12
|
+
def increment(cache_key, options, counter)
|
13
|
+
increment = options.fetch(:increment, 1)
|
14
|
+
Prop::Limiter.writer.call(cache_key, counter + increment)
|
15
|
+
end
|
16
|
+
|
17
|
+
def reset(cache_key)
|
18
|
+
Prop::Limiter.writer.call(cache_key, 0)
|
19
|
+
end
|
20
|
+
|
21
|
+
def at_threshold?(counter, options)
|
22
|
+
counter >= options.fetch(:threshold)
|
23
|
+
end
|
24
|
+
|
25
|
+
# Builds the expiring cache key
|
26
|
+
def build(options)
|
27
|
+
key = options.fetch(:key)
|
28
|
+
handle = options.fetch(:handle)
|
29
|
+
interval = options.fetch(:interval)
|
30
|
+
|
31
|
+
window = (Time.now.to_i / interval)
|
32
|
+
cache_key = Prop::Key.normalize([ handle, key, window ])
|
33
|
+
|
34
|
+
"prop/#{Digest::MD5.hexdigest(cache_key)}"
|
35
|
+
end
|
36
|
+
|
37
|
+
def threshold_reached(options)
|
38
|
+
threshold = options.fetch(:threshold)
|
39
|
+
|
40
|
+
"#{options[:handle]} threshold of #{threshold} tries per #{options[:interval]}s exceeded for key '#{options[:key].inspect}', hash #{options[:cache_key]}"
|
41
|
+
end
|
42
|
+
|
43
|
+
def validate_options!(options)
|
44
|
+
validate_positive_integer(options[:threshold], :threshold)
|
45
|
+
validate_positive_integer(options[:interval], :interval)
|
46
|
+
|
47
|
+
if options.key?(:increment)
|
48
|
+
raise ArgumentError.new(":increment must be zero or a positive Integer") if !options[:increment].is_a?(Fixnum) || options[:increment] < 0
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
|
54
|
+
def validate_positive_integer(option, key)
|
55
|
+
raise ArgumentError.new("#{key.inspect} must be a positive Integer") if !option.is_a?(Fixnum) || option <= 0
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
data/lib/prop/key.rb
CHANGED
@@ -3,18 +3,6 @@ require "digest/md5"
|
|
3
3
|
module Prop
|
4
4
|
class Key
|
5
5
|
|
6
|
-
# Builds the expiring cache key
|
7
|
-
def self.build(options)
|
8
|
-
key = options.fetch(:key)
|
9
|
-
handle = options.fetch(:handle)
|
10
|
-
interval = options.fetch(:interval)
|
11
|
-
|
12
|
-
window = (Time.now.to_i / interval)
|
13
|
-
cache_key = normalize([ handle, key, window ])
|
14
|
-
|
15
|
-
"prop/#{Digest::MD5.hexdigest(cache_key)}"
|
16
|
-
end
|
17
|
-
|
18
6
|
# Simple key expansion only supports arrays and primitives
|
19
7
|
def self.normalize(key)
|
20
8
|
if key.is_a?(Array)
|
@@ -0,0 +1,68 @@
|
|
1
|
+
require 'prop/limiter'
|
2
|
+
require 'prop/options'
|
3
|
+
require 'prop/key'
|
4
|
+
require 'prop/interval_strategy'
|
5
|
+
|
6
|
+
module Prop
|
7
|
+
class LeakyBucketStrategy
|
8
|
+
class << self
|
9
|
+
def update_bucket(cache_key, interval, leak_rate)
|
10
|
+
bucket = Prop::Limiter.reader.call(cache_key) || default_bucket
|
11
|
+
now = Time.now.to_i
|
12
|
+
leak_amount = (now - bucket[:last_updated]) / interval * leak_rate
|
13
|
+
|
14
|
+
bucket[:bucket] = [bucket[:bucket] - leak_amount, 0].max
|
15
|
+
bucket[:last_updated] = now
|
16
|
+
|
17
|
+
Prop::Limiter.writer.call(cache_key, bucket)
|
18
|
+
bucket
|
19
|
+
end
|
20
|
+
|
21
|
+
def counter(cache_key, options)
|
22
|
+
update_bucket(cache_key, options[:interval], options[:threshold]).merge(burst_rate: options[:burst_rate])
|
23
|
+
end
|
24
|
+
|
25
|
+
def increment(cache_key, options, counter)
|
26
|
+
increment = options.fetch(:increment, 1)
|
27
|
+
bucket = { :bucket => counter[:bucket].to_i + increment, :last_updated => Time.now.to_i }
|
28
|
+
Prop::Limiter.writer.call(cache_key, bucket)
|
29
|
+
end
|
30
|
+
|
31
|
+
def reset(cache_key)
|
32
|
+
Prop::Limiter.writer.call(cache_key, default_bucket)
|
33
|
+
end
|
34
|
+
|
35
|
+
def at_threshold?(counter, options)
|
36
|
+
counter[:bucket].to_i >= options.fetch(:burst_rate)
|
37
|
+
end
|
38
|
+
|
39
|
+
def build(options)
|
40
|
+
key = options.fetch(:key)
|
41
|
+
handle = options.fetch(:handle)
|
42
|
+
|
43
|
+
cache_key = Prop::Key.normalize([ handle, key ])
|
44
|
+
|
45
|
+
"prop/leaky_bucket/#{Digest::MD5.hexdigest(cache_key)}"
|
46
|
+
end
|
47
|
+
|
48
|
+
def default_bucket
|
49
|
+
{ :bucket => 0, :last_updated => 0 }
|
50
|
+
end
|
51
|
+
|
52
|
+
def threshold_reached(options)
|
53
|
+
burst_rate = options.fetch(:burst_rate)
|
54
|
+
threshold = options.fetch(:threshold)
|
55
|
+
|
56
|
+
"#{options[:handle]} threshold of #{threshold} tries per #{options[:interval]}s and burst rate #{burst_rate} tries exceeded for key '#{options[:key].inspect}', hash #{options[:cache_key]}"
|
57
|
+
end
|
58
|
+
|
59
|
+
def validate_options!(options)
|
60
|
+
Prop::IntervalStrategy.validate_options!(options)
|
61
|
+
|
62
|
+
if !options[:burst_rate].is_a?(Fixnum) || options[:burst_rate] < options[:threshold]
|
63
|
+
raise ArgumentError.new(":burst_rate must be an Integer and larger than :threshold")
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
data/lib/prop/limiter.rb
CHANGED
@@ -1,6 +1,8 @@
|
|
1
1
|
require 'prop/rate_limited'
|
2
2
|
require 'prop/key'
|
3
3
|
require 'prop/options'
|
4
|
+
require 'prop/interval_strategy'
|
5
|
+
require 'prop/leaky_bucket_strategy'
|
4
6
|
|
5
7
|
module Prop
|
6
8
|
class Limiter
|
@@ -54,18 +56,17 @@ module Prop
|
|
54
56
|
# Returns true if the threshold for this handle has been reached, else returns false
|
55
57
|
def throttle(handle, key = nil, options = {})
|
56
58
|
options, cache_key = prepare(handle, key, options)
|
57
|
-
counter =
|
59
|
+
counter = @strategy.counter(cache_key, options)
|
58
60
|
|
59
61
|
unless disabled?
|
60
|
-
if at_threshold?(counter, options
|
62
|
+
if @strategy.at_threshold?(counter, options)
|
61
63
|
unless before_throttle_callback.nil?
|
62
64
|
before_throttle_callback.call(handle, key, options[:threshold], options[:interval])
|
63
65
|
end
|
64
66
|
|
65
67
|
true
|
66
68
|
else
|
67
|
-
increment
|
68
|
-
writer.call(cache_key, counter + increment)
|
69
|
+
@strategy.increment(cache_key, options, counter)
|
69
70
|
|
70
71
|
yield if block_given?
|
71
72
|
|
@@ -90,7 +91,7 @@ module Prop
|
|
90
91
|
raise Prop::RateLimited.new(options.merge(:cache_key => cache_key, :handle => handle))
|
91
92
|
end
|
92
93
|
|
93
|
-
block_given? ? yield :
|
94
|
+
block_given? ? yield : @strategy.counter(cache_key, options)
|
94
95
|
end
|
95
96
|
|
96
97
|
# Public: Allows to query whether the given handle/key combination is currently throttled
|
@@ -101,7 +102,8 @@ module Prop
|
|
101
102
|
# Returns true if a call to `throttle!` with same parameters would raise, otherwise false
|
102
103
|
def throttled?(handle, key = nil, options = {})
|
103
104
|
options, cache_key = prepare(handle, key, options)
|
104
|
-
|
105
|
+
counter = @strategy.counter(cache_key, options)
|
106
|
+
@strategy.at_threshold?(counter, options)
|
105
107
|
end
|
106
108
|
|
107
109
|
# Public: Resets a specific throttle
|
@@ -112,7 +114,7 @@ module Prop
|
|
112
114
|
# Returns nothing
|
113
115
|
def reset(handle, key = nil, options = {})
|
114
116
|
options, cache_key = prepare(handle, key, options)
|
115
|
-
|
117
|
+
@strategy.reset(cache_key)
|
116
118
|
end
|
117
119
|
|
118
120
|
# Public: Counts the number of times the given handle/key combination has been hit in the current window
|
@@ -123,7 +125,7 @@ module Prop
|
|
123
125
|
# Returns a count of hits in the current window
|
124
126
|
def count(handle, key = nil, options = {})
|
125
127
|
options, cache_key = prepare(handle, key, options)
|
126
|
-
|
128
|
+
@strategy.counter(cache_key, options)
|
127
129
|
end
|
128
130
|
alias :query :count
|
129
131
|
|
@@ -134,10 +136,6 @@ module Prop
|
|
134
136
|
|
135
137
|
private
|
136
138
|
|
137
|
-
def at_threshold?(mark, threshold)
|
138
|
-
mark >= threshold
|
139
|
-
end
|
140
|
-
|
141
139
|
def disabled?
|
142
140
|
!!@disabled
|
143
141
|
end
|
@@ -147,7 +145,10 @@ module Prop
|
|
147
145
|
|
148
146
|
defaults = handles[handle]
|
149
147
|
options = Prop::Options.build(:key => key, :params => params, :defaults => defaults)
|
150
|
-
|
148
|
+
|
149
|
+
@strategy = options.fetch(:strategy)
|
150
|
+
|
151
|
+
cache_key = @strategy.build(:key => key, :handle => handle, :interval => options[:interval])
|
151
152
|
|
152
153
|
[ options, cache_key ]
|
153
154
|
end
|
data/lib/prop/options.rb
CHANGED
@@ -10,15 +10,22 @@ module Prop
|
|
10
10
|
defaults = options.fetch(:defaults)
|
11
11
|
result = defaults.merge(params)
|
12
12
|
|
13
|
-
result[:key]
|
14
|
-
result[:threshold] = result[:threshold].to_i
|
15
|
-
result[:interval] = result[:interval].to_i
|
13
|
+
result[:key] = Prop::Key.normalize(key)
|
16
14
|
|
17
|
-
|
18
|
-
|
15
|
+
result[:strategy] = if leaky_bucket.include?(result[:strategy])
|
16
|
+
Prop::LeakyBucketStrategy
|
17
|
+
elsif result[:strategy] == nil
|
18
|
+
Prop::IntervalStrategy
|
19
|
+
else
|
20
|
+
result[:strategy] # allowing any new/unknown strategy to be used
|
21
|
+
end
|
19
22
|
|
23
|
+
result[:strategy].validate_options!(result)
|
20
24
|
result
|
21
25
|
end
|
22
26
|
|
27
|
+
def self.leaky_bucket
|
28
|
+
[:leaky_bucket, "leaky_bucket"]
|
29
|
+
end
|
23
30
|
end
|
24
|
-
end
|
31
|
+
end
|
data/lib/prop/rate_limited.rb
CHANGED
@@ -6,9 +6,9 @@ module Prop
|
|
6
6
|
handle = options.fetch(:handle)
|
7
7
|
cache_key = options.fetch(:cache_key)
|
8
8
|
interval = options.fetch(:interval).to_i
|
9
|
-
|
9
|
+
strategy = options.fetch(:strategy)
|
10
10
|
|
11
|
-
super(
|
11
|
+
super(strategy.threshold_reached(options))
|
12
12
|
|
13
13
|
self.description = options[:description]
|
14
14
|
self.handle = handle
|
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: 1.
|
4
|
+
version: 1.2.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: 2015-07-09 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rake
|
@@ -66,35 +66,23 @@ dependencies:
|
|
66
66
|
- - ">="
|
67
67
|
- !ruby/object:Gem::Version
|
68
68
|
version: '0'
|
69
|
-
description:
|
69
|
+
description:
|
70
70
|
email: primdahl@me.com
|
71
71
|
executables: []
|
72
72
|
extensions: []
|
73
73
|
extra_rdoc_files: []
|
74
74
|
files:
|
75
|
-
- ".document"
|
76
|
-
- ".gemtest"
|
77
|
-
- ".gitignore"
|
78
|
-
- ".travis.yml"
|
79
|
-
- Gemfile
|
80
75
|
- LICENSE
|
81
76
|
- README.md
|
82
|
-
- Rakefile
|
83
77
|
- lib/prop.rb
|
78
|
+
- lib/prop/interval_strategy.rb
|
84
79
|
- lib/prop/key.rb
|
80
|
+
- lib/prop/leaky_bucket_strategy.rb
|
85
81
|
- lib/prop/limiter.rb
|
86
82
|
- lib/prop/middleware.rb
|
87
83
|
- lib/prop/options.rb
|
88
84
|
- lib/prop/rate_limited.rb
|
89
|
-
|
90
|
-
- test/helper.rb
|
91
|
-
- test/test_key.rb
|
92
|
-
- test/test_limiter.rb
|
93
|
-
- test/test_middleware.rb
|
94
|
-
- test/test_options.rb
|
95
|
-
- test/test_prop.rb
|
96
|
-
- test/test_rate_limited.rb
|
97
|
-
homepage: http://github.com/zendesk/prop
|
85
|
+
homepage: https://github.com/zendesk/prop
|
98
86
|
licenses:
|
99
87
|
- Apache License Version 2.0
|
100
88
|
metadata: {}
|
@@ -113,15 +101,9 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
113
101
|
- !ruby/object:Gem::Version
|
114
102
|
version: '0'
|
115
103
|
requirements: []
|
116
|
-
rubyforge_project:
|
117
|
-
rubygems_version: 2.
|
104
|
+
rubyforge_project:
|
105
|
+
rubygems_version: 2.4.7
|
118
106
|
signing_key:
|
119
107
|
specification_version: 4
|
120
108
|
summary: Gem for implementing rate limits.
|
121
|
-
test_files:
|
122
|
-
- test/test_key.rb
|
123
|
-
- test/test_limiter.rb
|
124
|
-
- test/test_middleware.rb
|
125
|
-
- test/test_options.rb
|
126
|
-
- test/test_prop.rb
|
127
|
-
- test/test_rate_limited.rb
|
109
|
+
test_files: []
|
data/.document
DELETED
data/.gemtest
DELETED
File without changes
|
data/.gitignore
DELETED
data/.travis.yml
DELETED
data/Gemfile
DELETED
data/Rakefile
DELETED
data/prop.gemspec
DELETED
@@ -1,26 +0,0 @@
|
|
1
|
-
Gem::Specification.new "prop", "1.1.0" do |s|
|
2
|
-
s.name = 'prop'
|
3
|
-
s.version = '1.1.0'
|
4
|
-
s.date = '2014-12-31'
|
5
|
-
s.rubyforge_project = 'prop'
|
6
|
-
s.license = "Apache License Version 2.0"
|
7
|
-
|
8
|
-
s.summary = "Gem for implementing rate limits."
|
9
|
-
s.description = "Gem for implementing rate limits."
|
10
|
-
|
11
|
-
s.authors = ["Morten Primdahl"]
|
12
|
-
s.email = 'primdahl@me.com'
|
13
|
-
s.homepage = 'http://github.com/zendesk/prop'
|
14
|
-
|
15
|
-
## This gets added to the $LOAD_PATH so that 'lib/NAME.rb' can be required as
|
16
|
-
## require 'NAME.rb' or'/lib/NAME/file.rb' can be as require 'NAME/file.rb'
|
17
|
-
s.require_paths = %w[lib]
|
18
|
-
|
19
|
-
s.add_development_dependency('rake')
|
20
|
-
s.add_development_dependency('bundler')
|
21
|
-
s.add_development_dependency('minitest')
|
22
|
-
s.add_development_dependency('mocha')
|
23
|
-
|
24
|
-
s.files = `git ls-files`.split("\n")
|
25
|
-
s.test_files = s.files.select { |path| path =~ /^test\/test_.*\.rb/ }
|
26
|
-
end
|
data/test/helper.rb
DELETED
data/test/test_key.rb
DELETED
@@ -1,24 +0,0 @@
|
|
1
|
-
require 'helper'
|
2
|
-
|
3
|
-
describe Prop::Key do
|
4
|
-
describe "#build" do
|
5
|
-
it "return a hexdigested key" do
|
6
|
-
assert_match /prop\/[a-f0-9]+/, Prop::Key.build(:handle => :hello, :key => [ "foo", 2, :bar ], :interval => 60)
|
7
|
-
end
|
8
|
-
end
|
9
|
-
|
10
|
-
describe "#normalize" do
|
11
|
-
it "turn a Fixnum into a String" do
|
12
|
-
assert_equal "3", Prop::Key.normalize(3)
|
13
|
-
end
|
14
|
-
|
15
|
-
it "return a String" do
|
16
|
-
assert_equal "S", Prop::Key.normalize("S")
|
17
|
-
end
|
18
|
-
|
19
|
-
it "flatten and join an Array" do
|
20
|
-
assert_equal "1/B/3", Prop::Key.normalize([ 1, "B", "3" ])
|
21
|
-
end
|
22
|
-
end
|
23
|
-
end
|
24
|
-
|
data/test/test_limiter.rb
DELETED
@@ -1,153 +0,0 @@
|
|
1
|
-
require 'helper'
|
2
|
-
|
3
|
-
describe Prop::Limiter do
|
4
|
-
before do
|
5
|
-
@store = {}
|
6
|
-
|
7
|
-
Prop::Limiter.read { |key| @store[key] }
|
8
|
-
Prop::Limiter.write { |key, value| @store[key] = value }
|
9
|
-
Prop::Limiter.configure(:something, :threshold => 10, :interval => 10)
|
10
|
-
|
11
|
-
@start = Time.now
|
12
|
-
Time.stubs(:now).returns(@start)
|
13
|
-
|
14
|
-
Prop.reset(:something)
|
15
|
-
end
|
16
|
-
|
17
|
-
describe "#throttle" do
|
18
|
-
describe "when disabled" do
|
19
|
-
before { Prop::Limiter.stubs(:disabled?).returns(true) }
|
20
|
-
|
21
|
-
it "returns nil" do
|
22
|
-
assert_nil Prop.throttle(:something)
|
23
|
-
end
|
24
|
-
end
|
25
|
-
|
26
|
-
describe "when not disabled" do
|
27
|
-
before { Prop::Limiter.stubs(:disabled?).returns(false) }
|
28
|
-
|
29
|
-
describe "and the threshold has been reached" do
|
30
|
-
before { Prop::Limiter.stubs(:at_threshold?).returns(true) }
|
31
|
-
|
32
|
-
it "returns true" do
|
33
|
-
assert Prop.throttle(:something)
|
34
|
-
end
|
35
|
-
|
36
|
-
it "does not increment the throttle count" do
|
37
|
-
Prop.throttle(:something)
|
38
|
-
|
39
|
-
assert_equal 0, Prop.count(:something)
|
40
|
-
end
|
41
|
-
|
42
|
-
describe "when given a block" do
|
43
|
-
before { @test_block_executed = false }
|
44
|
-
|
45
|
-
it "does not execute the block" do
|
46
|
-
Prop.throttle(:something) { @test_block_executed = true }
|
47
|
-
|
48
|
-
refute @test_block_executed
|
49
|
-
end
|
50
|
-
end
|
51
|
-
|
52
|
-
describe "when a before_throttle callback has been specified" do
|
53
|
-
before do
|
54
|
-
Prop.before_throttle do |handle, key, threshold, interval|
|
55
|
-
@handle = handle
|
56
|
-
@key = key
|
57
|
-
@threshold = threshold
|
58
|
-
@interval = interval
|
59
|
-
end
|
60
|
-
|
61
|
-
Prop.throttle(:something, [:extra])
|
62
|
-
end
|
63
|
-
|
64
|
-
it "invokes callback with expected parameters" do
|
65
|
-
assert_equal @handle, :something
|
66
|
-
assert_equal @key, [:extra]
|
67
|
-
assert_equal @threshold, 10
|
68
|
-
assert_equal @interval, 10
|
69
|
-
end
|
70
|
-
end
|
71
|
-
end
|
72
|
-
|
73
|
-
describe "and the threshold has not been reached" do
|
74
|
-
before { Prop::Limiter.stubs(:at_threshold?).returns(false) }
|
75
|
-
|
76
|
-
it "returns false" do
|
77
|
-
refute Prop.throttle(:something)
|
78
|
-
end
|
79
|
-
|
80
|
-
it "increments the throttle count by one" do
|
81
|
-
Prop.throttle(:something)
|
82
|
-
|
83
|
-
assert_equal 1, Prop.count(:something)
|
84
|
-
end
|
85
|
-
|
86
|
-
it "increments the throttle count by the specified number when provided" do
|
87
|
-
Prop.throttle(:something, nil, :increment => 5)
|
88
|
-
|
89
|
-
assert_equal 5, Prop.count(:something)
|
90
|
-
end
|
91
|
-
|
92
|
-
describe "when given a block" do
|
93
|
-
before { @test_block_executed = false }
|
94
|
-
|
95
|
-
it "executes the block" do
|
96
|
-
Prop.throttle(:something) { @test_block_executed = true }
|
97
|
-
|
98
|
-
assert @test_block_executed
|
99
|
-
end
|
100
|
-
end
|
101
|
-
end
|
102
|
-
end
|
103
|
-
end
|
104
|
-
|
105
|
-
describe "#throttle!" do
|
106
|
-
it "throttles the given handle/key combination" do
|
107
|
-
Prop::Limiter.expects(:throttle).with(
|
108
|
-
:something,
|
109
|
-
:key,
|
110
|
-
{
|
111
|
-
:threshold => 10,
|
112
|
-
:interval => 10,
|
113
|
-
:key => 'key',
|
114
|
-
:options => true
|
115
|
-
}
|
116
|
-
)
|
117
|
-
|
118
|
-
Prop.throttle!(:something, :key, :options => true)
|
119
|
-
end
|
120
|
-
|
121
|
-
describe "when the threshold has been reached" do
|
122
|
-
before { Prop::Limiter.stubs(:throttle).returns(true) }
|
123
|
-
|
124
|
-
it "raises a rate-limited exception" do
|
125
|
-
assert_raises(Prop::RateLimited) { Prop.throttle!(:something) }
|
126
|
-
end
|
127
|
-
|
128
|
-
describe "when given a block" do
|
129
|
-
before { @test_block_executed = false }
|
130
|
-
|
131
|
-
it "does not executes the block" do
|
132
|
-
begin
|
133
|
-
Prop.throttle!(:something) { @test_block_executed = true }
|
134
|
-
rescue Prop::RateLimited
|
135
|
-
refute @test_block_executed
|
136
|
-
end
|
137
|
-
end
|
138
|
-
end
|
139
|
-
end
|
140
|
-
|
141
|
-
describe "when the threshold has not been reached" do
|
142
|
-
it "returns the counter value" do
|
143
|
-
assert_equal Prop.count(:something) + 1, Prop.throttle!(:something)
|
144
|
-
end
|
145
|
-
|
146
|
-
describe "when given a block" do
|
147
|
-
it "returns the return value of the block" do
|
148
|
-
assert_equal 'block_value', Prop.throttle!(:something) { 'block_value' }
|
149
|
-
end
|
150
|
-
end
|
151
|
-
end
|
152
|
-
end
|
153
|
-
end
|
data/test/test_middleware.rb
DELETED
@@ -1,46 +0,0 @@
|
|
1
|
-
require 'helper'
|
2
|
-
|
3
|
-
require 'prop/middleware'
|
4
|
-
require 'prop/rate_limited'
|
5
|
-
|
6
|
-
|
7
|
-
describe Prop::Middleware do
|
8
|
-
before do
|
9
|
-
@app = stub()
|
10
|
-
@env = {}
|
11
|
-
@middleware = Prop::Middleware.new(@app)
|
12
|
-
end
|
13
|
-
|
14
|
-
describe "when the app call completes" do
|
15
|
-
before do
|
16
|
-
@app.expects(:call).with(@env).returns("response")
|
17
|
-
end
|
18
|
-
|
19
|
-
it "return the response" do
|
20
|
-
assert_equal "response", @middleware.call(@env)
|
21
|
-
end
|
22
|
-
end
|
23
|
-
|
24
|
-
describe "when the app call results in a raised throttle" do
|
25
|
-
before do
|
26
|
-
@app.expects(:call).with(@env).raises(Prop::RateLimited.new(:handle => "foo", :threshold => 10, :interval => 60, :cache_key => "wibble", :description => "Boom!"))
|
27
|
-
end
|
28
|
-
|
29
|
-
it "return the rate limited message" do
|
30
|
-
response = @middleware.call(@env)
|
31
|
-
|
32
|
-
assert_equal 429, response[0]
|
33
|
-
assert_equal ["Boom!"], response[2]
|
34
|
-
end
|
35
|
-
|
36
|
-
describe "with a custom error handler" do
|
37
|
-
before do
|
38
|
-
@middleware = Prop::Middleware.new(@app, :error_handler => Proc.new { |env, error| "Oops" })
|
39
|
-
end
|
40
|
-
|
41
|
-
it "allow setting a custom error handler" do
|
42
|
-
assert_equal "Oops", @middleware.call(@env)
|
43
|
-
end
|
44
|
-
end
|
45
|
-
end
|
46
|
-
end
|
data/test/test_options.rb
DELETED
@@ -1,44 +0,0 @@
|
|
1
|
-
require 'helper'
|
2
|
-
|
3
|
-
describe Prop::Options do
|
4
|
-
describe "#build" do
|
5
|
-
before do
|
6
|
-
@args = { :key => "hello", :params => { :foo => "bif" }, :defaults => { :foo => "bar", :baz => "moo", :threshold => 10, :interval => 5 }}
|
7
|
-
end
|
8
|
-
|
9
|
-
describe "when given valid input" do
|
10
|
-
before do
|
11
|
-
@options = Prop::Options.build(@args)
|
12
|
-
end
|
13
|
-
|
14
|
-
it "support defaults" do
|
15
|
-
assert_equal "moo", @options[:baz]
|
16
|
-
end
|
17
|
-
|
18
|
-
it "override defaults" do
|
19
|
-
assert_equal "bif", @options[:foo]
|
20
|
-
end
|
21
|
-
end
|
22
|
-
|
23
|
-
describe "when given invalid input" do
|
24
|
-
it "raise when not given an interval" do
|
25
|
-
@args[:defaults].delete(:interval)
|
26
|
-
assert_raises(RuntimeError) { Prop::Options.build(@args) }
|
27
|
-
end
|
28
|
-
|
29
|
-
it "raise when not given a threshold" do
|
30
|
-
@args[:defaults].delete(:threshold)
|
31
|
-
assert_raises(RuntimeError) { Prop::Options.build(@args) }
|
32
|
-
end
|
33
|
-
|
34
|
-
it "raise when not given a key" do
|
35
|
-
@args.delete(:key)
|
36
|
-
begin
|
37
|
-
Prop::Options.build(@args)
|
38
|
-
fail "it puke when not given a valid key"
|
39
|
-
rescue
|
40
|
-
end
|
41
|
-
end
|
42
|
-
end
|
43
|
-
end
|
44
|
-
end
|
data/test/test_prop.rb
DELETED
@@ -1,205 +0,0 @@
|
|
1
|
-
require 'helper'
|
2
|
-
|
3
|
-
# Integration level tests
|
4
|
-
describe Prop do
|
5
|
-
before do
|
6
|
-
store = {}
|
7
|
-
Prop.read { |key| store[key] }
|
8
|
-
Prop.write { |key, value| store[key] = value }
|
9
|
-
|
10
|
-
@start = Time.now
|
11
|
-
Time.stubs(:now).returns(@start)
|
12
|
-
end
|
13
|
-
|
14
|
-
describe "#defaults" do
|
15
|
-
it "raise errors on invalid configuation" do
|
16
|
-
assert_raises(RuntimeError) do
|
17
|
-
Prop.configure :hello_there, :threshold => 20, :interval => 'hello'
|
18
|
-
end
|
19
|
-
|
20
|
-
assert_raises(RuntimeError) do
|
21
|
-
Prop.configure :hello_there, :threshold => 'wibble', :interval => 100
|
22
|
-
end
|
23
|
-
end
|
24
|
-
|
25
|
-
it "result in a default handle" do
|
26
|
-
Prop.configure :hello_there, :threshold => 4, :interval => 10
|
27
|
-
4.times do |i|
|
28
|
-
assert_equal (i + 1), Prop.throttle!(:hello_there, 'some key')
|
29
|
-
end
|
30
|
-
|
31
|
-
assert_raises(Prop::RateLimited) { Prop.throttle!(:hello_there, 'some key') }
|
32
|
-
assert_equal 5, Prop.throttle!(:hello_there, 'some key', :threshold => 20)
|
33
|
-
end
|
34
|
-
|
35
|
-
it "create a handle accepts various cache key types" do
|
36
|
-
Prop.configure :hello_there, :threshold => 4, :interval => 10
|
37
|
-
assert_equal 1, Prop.throttle!(:hello_there, 5)
|
38
|
-
assert_equal 2, Prop.throttle!(:hello_there, 5)
|
39
|
-
assert_equal 1, Prop.throttle!(:hello_there, '6')
|
40
|
-
assert_equal 2, Prop.throttle!(:hello_there, '6')
|
41
|
-
assert_equal 1, Prop.throttle!(:hello_there, [ 5, '6' ])
|
42
|
-
assert_equal 2, Prop.throttle!(:hello_there, [ 5, '6' ])
|
43
|
-
end
|
44
|
-
end
|
45
|
-
|
46
|
-
describe "#disable" do
|
47
|
-
before do
|
48
|
-
Prop.configure :hello, :threshold => 10, :interval => 10
|
49
|
-
end
|
50
|
-
|
51
|
-
it "not increase the throttle" do
|
52
|
-
assert_equal 1, Prop.throttle!(:hello)
|
53
|
-
assert_equal 2, Prop.throttle!(:hello)
|
54
|
-
Prop.disabled do
|
55
|
-
assert_equal 2, Prop.throttle!(:hello)
|
56
|
-
assert_equal 2, Prop.throttle!(:hello)
|
57
|
-
assert Prop::Limiter.send(:disabled?)
|
58
|
-
end
|
59
|
-
assert !Prop::Limiter.send(:disabled?)
|
60
|
-
assert_equal 3, Prop.throttle!(:hello)
|
61
|
-
end
|
62
|
-
end
|
63
|
-
|
64
|
-
describe "#reset" do
|
65
|
-
before do
|
66
|
-
Prop.configure :hello, :threshold => 10, :interval => 10
|
67
|
-
|
68
|
-
5.times do |i|
|
69
|
-
assert_equal (i + 1), Prop.throttle!(:hello)
|
70
|
-
end
|
71
|
-
end
|
72
|
-
|
73
|
-
it "set the correct counter to 0" do
|
74
|
-
Prop.throttle!(:hello, 'wibble')
|
75
|
-
Prop.throttle!(:hello, 'wibble')
|
76
|
-
|
77
|
-
Prop.reset(:hello)
|
78
|
-
assert_equal 1, Prop.throttle!(:hello)
|
79
|
-
|
80
|
-
assert_equal 3, Prop.throttle!(:hello, 'wibble')
|
81
|
-
Prop.reset(:hello, 'wibble')
|
82
|
-
assert_equal 1, Prop.throttle!(:hello, 'wibble')
|
83
|
-
end
|
84
|
-
end
|
85
|
-
|
86
|
-
describe "#throttled?" do
|
87
|
-
it "return true once the threshold has been reached" do
|
88
|
-
Prop.configure(:hello, :threshold => 2, :interval => 10)
|
89
|
-
Prop.throttle!(:hello)
|
90
|
-
assert !Prop.throttled?(:hello)
|
91
|
-
Prop.throttle!(:hello)
|
92
|
-
assert Prop.throttled?(:hello)
|
93
|
-
end
|
94
|
-
end
|
95
|
-
|
96
|
-
describe "#count" do
|
97
|
-
before do
|
98
|
-
Prop.configure(:hello, :threshold => 20, :interval => 20)
|
99
|
-
Prop.throttle!(:hello)
|
100
|
-
Prop.throttle!(:hello)
|
101
|
-
end
|
102
|
-
|
103
|
-
it "be aliased by #count" do
|
104
|
-
assert_equal Prop.count(:hello), 2
|
105
|
-
end
|
106
|
-
|
107
|
-
it "return the number of hits on a throttle" do
|
108
|
-
assert_equal Prop.query(:hello), 2
|
109
|
-
end
|
110
|
-
end
|
111
|
-
|
112
|
-
describe "#throttle!" do
|
113
|
-
it "increment counter correctly" do
|
114
|
-
Prop.configure(:hello, :threshold => 20, :interval => 20)
|
115
|
-
3.times do |i|
|
116
|
-
assert_equal (i + 1), Prop.throttle!(:hello, nil, :threshold => 10, :interval => 10)
|
117
|
-
end
|
118
|
-
end
|
119
|
-
|
120
|
-
it "reset counter when time window is passed" do
|
121
|
-
Prop.configure(:hello, :threshold => 20, :interval => 20)
|
122
|
-
3.times do |i|
|
123
|
-
assert_equal (i + 1), Prop.throttle!(:hello, nil, :threshold => 10, :interval => 10)
|
124
|
-
end
|
125
|
-
|
126
|
-
Time.stubs(:now).returns(@start + 20)
|
127
|
-
|
128
|
-
3.times do |i|
|
129
|
-
assert_equal (i + 1), Prop.throttle!(:hello, nil, :threshold => 10, :interval => 10)
|
130
|
-
end
|
131
|
-
end
|
132
|
-
|
133
|
-
it "not increment the counter beyond the threshold" do
|
134
|
-
Prop.configure(:hello, :threshold => 5, :interval => 1)
|
135
|
-
10.times do |i|
|
136
|
-
Prop.throttle!(:hello) rescue nil
|
137
|
-
end
|
138
|
-
|
139
|
-
assert_equal 5, Prop.query(:hello)
|
140
|
-
end
|
141
|
-
|
142
|
-
it "support custom increments" do
|
143
|
-
Prop.configure(:hello, :threshold => 100, :interval => 10)
|
144
|
-
|
145
|
-
Prop.throttle!(:hello)
|
146
|
-
Prop.throttle!(:hello)
|
147
|
-
|
148
|
-
assert_equal 2, Prop.query(:hello)
|
149
|
-
|
150
|
-
Prop.throttle!(:hello, nil, :increment => 48)
|
151
|
-
|
152
|
-
assert_equal 50, Prop.query(:hello)
|
153
|
-
end
|
154
|
-
|
155
|
-
it "raise Prop::RateLimited when the threshold is exceeded" do
|
156
|
-
Prop.configure(:hello, :threshold => 5, :interval => 10, :description => "Boom!")
|
157
|
-
|
158
|
-
5.times do |i|
|
159
|
-
Prop.throttle!(:hello, nil)
|
160
|
-
end
|
161
|
-
assert_raises(Prop::RateLimited) do
|
162
|
-
Prop.throttle!(:hello, nil)
|
163
|
-
end
|
164
|
-
|
165
|
-
begin
|
166
|
-
Prop.throttle!(:hello, nil)
|
167
|
-
fail
|
168
|
-
rescue Prop::RateLimited => e
|
169
|
-
assert_equal :hello, e.handle
|
170
|
-
assert_match "5 tries per 10s exceeded for key", e.message
|
171
|
-
assert_equal "Boom!", e.description
|
172
|
-
assert e.retry_after
|
173
|
-
end
|
174
|
-
end
|
175
|
-
|
176
|
-
it "raise a RuntimeError when a handle has not been configured" do
|
177
|
-
assert_raises(RuntimeError) do
|
178
|
-
Prop.throttle!(:no_such_handle, nil, :threshold => 5, :interval => 10)
|
179
|
-
end
|
180
|
-
end
|
181
|
-
end
|
182
|
-
|
183
|
-
describe 'different handles with the same interval' do
|
184
|
-
before do
|
185
|
-
Prop.configure(:api_requests, :threshold => 100, :interval => 30)
|
186
|
-
Prop.configure(:login_attempts, :threshold => 10, :interval => 30)
|
187
|
-
end
|
188
|
-
|
189
|
-
it 'be counted separately' do
|
190
|
-
user_id = 42
|
191
|
-
Prop.throttle!(:api_requests, user_id)
|
192
|
-
assert_equal(1, Prop.count(:api_requests, user_id))
|
193
|
-
assert_equal(0, Prop.count(:login_attempts, user_id))
|
194
|
-
end
|
195
|
-
end
|
196
|
-
|
197
|
-
describe "#configurations" do
|
198
|
-
it "returns the configuration" do
|
199
|
-
Prop.configure(:something, :threshold => 100, :interval => 30)
|
200
|
-
config = Prop.configurations[:something]
|
201
|
-
assert_equal 100, config[:threshold]
|
202
|
-
assert_equal 30, config[:interval]
|
203
|
-
end
|
204
|
-
end
|
205
|
-
end
|
data/test/test_rate_limited.rb
DELETED
@@ -1,39 +0,0 @@
|
|
1
|
-
require 'helper'
|
2
|
-
|
3
|
-
describe Prop::RateLimited do
|
4
|
-
before do
|
5
|
-
time = Time.at(1333685680)
|
6
|
-
Time.stubs(:now).returns(time)
|
7
|
-
|
8
|
-
Prop.configure :foo, :threshold => 10, :interval => 60, :category => :api
|
9
|
-
|
10
|
-
@error = Prop::RateLimited.new(
|
11
|
-
:handle => :foo,
|
12
|
-
:threshold => 10,
|
13
|
-
:interval => 60,
|
14
|
-
:cache_key => "wibble",
|
15
|
-
:description => "Boom!"
|
16
|
-
)
|
17
|
-
end
|
18
|
-
|
19
|
-
describe "#initialize" do
|
20
|
-
it "returns an error instance" do
|
21
|
-
assert @error.is_a?(StandardError)
|
22
|
-
assert @error.is_a?(Prop::RateLimited)
|
23
|
-
|
24
|
-
assert_equal :foo, @error.handle
|
25
|
-
assert_equal "wibble", @error.cache_key
|
26
|
-
assert_equal "Boom!", @error.description
|
27
|
-
assert_equal "foo threshold of 10 tries per 60s exceeded for key 'nil', hash wibble", @error.message
|
28
|
-
assert_equal 20, @error.retry_after
|
29
|
-
end
|
30
|
-
end
|
31
|
-
|
32
|
-
describe "#config" do
|
33
|
-
it "returns the original configuration" do
|
34
|
-
assert_equal 10, @error.config[:threshold]
|
35
|
-
assert_equal 60, @error.config[:interval]
|
36
|
-
assert_equal :api, @error.config[:category]
|
37
|
-
end
|
38
|
-
end
|
39
|
-
end
|