pause 0.0.3 → 0.0.4
Sign up to get free protection for your applications and to get access to all the features.
- data/.travis.yml +10 -0
- data/README.md +34 -15
- data/lib/pause.rb +2 -1
- data/lib/pause/action.rb +20 -12
- data/lib/pause/analyzer.rb +4 -4
- data/lib/pause/{blocked_action.rb → rate_limited_event.rb} +1 -1
- data/lib/pause/redis/adapter.rb +11 -11
- data/lib/pause/version.rb +1 -1
- data/pause.gemspec +1 -1
- data/spec/pause/action_spec.rb +38 -56
- data/spec/pause/analyzer_spec.rb +2 -2
- data/spec/pause/redis/adapter_spec.rb +3 -3
- data/spec/spec_helper.rb +1 -2
- metadata +4 -4
data/.travis.yml
ADDED
data/README.md
CHANGED
@@ -1,6 +1,8 @@
|
|
1
1
|
Pause
|
2
2
|
======
|
3
3
|
|
4
|
+
[![Build status](https://secure.travis-ci.org/wanelo/pause.png)](http://travis-ci.org/wanelo/pause)
|
5
|
+
|
4
6
|
Pause is a Redis-backed rate-limiting client. Use it to track events, with
|
5
7
|
rules around how often they are allowed to occur within configured time checks.
|
6
8
|
|
@@ -20,6 +22,8 @@ Or install it yourself as:
|
|
20
22
|
|
21
23
|
## Usage
|
22
24
|
|
25
|
+
### Configuration
|
26
|
+
|
23
27
|
Configure Pause. This could be in a Rails initializer.
|
24
28
|
|
25
29
|
* resolution - The time resolution (in seconds) defining the minimum period into which action counts are
|
@@ -38,26 +42,40 @@ Pause.configure do |config|
|
|
38
42
|
end
|
39
43
|
```
|
40
44
|
|
41
|
-
|
42
|
-
|
45
|
+
### Actions
|
46
|
+
|
47
|
+
Define local actions for your application. Actions define a scope by
|
48
|
+
which they are identified in the persistent store, and a set of checks. Checks define various
|
49
|
+
thresholds (`max_allowed`) against periods of time (`period_seconds`). When a threshold it triggered,
|
50
|
+
the action is rate limited, and stays rate limited for the duration of `block_ttl` seconds.
|
51
|
+
|
52
|
+
#### Checks
|
53
|
+
|
54
|
+
Checks are configured with the following arguments (which can be passed as an array, or a symbol hash):
|
55
|
+
|
56
|
+
* `period_seconds` - time window this is a time period against which an action is tested
|
57
|
+
* `max_allowed` - the maximum number of times an action can be incremented during this particular time period before rate limiting is triggered.
|
58
|
+
* `block_ttl` - amount time (seconds) an action stays rate limited after threshold is reached.
|
59
|
+
|
60
|
+
#### Scope
|
61
|
+
|
62
|
+
Scope is simple string used to identify this action in the Redis store, and is appended to all keys.
|
63
|
+
Therefore it is advised to keep scope as short as possible to reduce memory requirements of the store.
|
43
64
|
|
44
|
-
|
65
|
+
#### Resolution
|
45
66
|
|
46
|
-
|
47
|
-
|
48
|
-
period seconds
|
49
|
-
* `block_ttl` - how long to mark an action as blocked if it goes over max-allowed
|
67
|
+
Note that your resolution must be less than or equal to the smallest `period_seconds` value in your checks.
|
68
|
+
In other words, if your shortest check is 1 minute, you could set resolution to 1 minute or smaller.
|
50
69
|
|
51
|
-
|
52
|
-
Pause config. If you do so, you will actually be checking sums against the full time period.
|
70
|
+
#### Example
|
53
71
|
|
54
72
|
```ruby
|
55
73
|
require 'pause'
|
56
74
|
|
57
75
|
class FollowAction < Pause::Action
|
58
|
-
scope "
|
59
|
-
check
|
60
|
-
check
|
76
|
+
scope "f"
|
77
|
+
check period_seconds: 60, max_allowed: 100, block_ttl: 3600
|
78
|
+
check period_seconds: 1800, max_allowed: 2000, block_ttl: 3600
|
61
79
|
end
|
62
80
|
```
|
63
81
|
|
@@ -69,10 +87,11 @@ class FollowsController < ApplicationController
|
|
69
87
|
def create
|
70
88
|
action = FollowAction.new(user.id)
|
71
89
|
if action.ok?
|
72
|
-
# do stuff
|
90
|
+
# do stuff!
|
73
91
|
action.increment!
|
74
92
|
else
|
75
|
-
#
|
93
|
+
# action is rate limited, either skip
|
94
|
+
# or show error, depending on the context.
|
76
95
|
end
|
77
96
|
end
|
78
97
|
end
|
@@ -81,7 +100,7 @@ class OtherController < ApplicationController
|
|
81
100
|
def index
|
82
101
|
action = OtherAction.new(params[:thing])
|
83
102
|
if action.ok?
|
84
|
-
action.increment!(
|
103
|
+
action.increment!(params[:count].to_i, Time.now.to_i)
|
85
104
|
end
|
86
105
|
end
|
87
106
|
end
|
data/lib/pause.rb
CHANGED
@@ -1,9 +1,10 @@
|
|
1
|
+
require 'redis'
|
1
2
|
require "pause/version"
|
2
3
|
require "pause/configuration"
|
3
4
|
require "pause/action"
|
4
5
|
require "pause/analyzer"
|
5
6
|
require "pause/redis/adapter"
|
6
|
-
require 'pause/
|
7
|
+
require 'pause/rate_limited_event'
|
7
8
|
|
8
9
|
module Pause
|
9
10
|
class PeriodCheck < Struct.new(:period_seconds, :max_allowed, :block_ttl)
|
data/lib/pause/action.rb
CHANGED
@@ -25,17 +25,23 @@ module Pause
|
|
25
25
|
# Action subclasses should define their checks as follows
|
26
26
|
#
|
27
27
|
# period_seconds - compare all activity by an identifier within the time period
|
28
|
-
# max_allowed
|
29
|
-
#
|
30
|
-
#
|
28
|
+
# max_allowed - if the number of actions by an identifier exceeds max_allowed for the time period marked
|
29
|
+
# by period_seconds, it is no longer ok.
|
30
|
+
# block_ttl - time to mark identifier as not ok
|
31
31
|
#
|
32
32
|
# class MyAction < Pause::Action
|
33
|
-
# check
|
34
|
-
# check
|
33
|
+
# check period_seconds: 60, max_allowed: 100, block_ttl: 3600
|
34
|
+
# check period_seconds: 1800, max_allowed: 2000, block_ttl: 3600
|
35
35
|
# end
|
36
36
|
#
|
37
|
-
def self.check(
|
37
|
+
def self.check(*args)
|
38
38
|
@checks ||= []
|
39
|
+
period_seconds, max_allowed, block_ttl =
|
40
|
+
if args.first.is_a?(Hash)
|
41
|
+
[args.first[:period_seconds], args.first[:max_allowed], args.first[:block_ttl]]
|
42
|
+
else
|
43
|
+
args
|
44
|
+
end
|
39
45
|
@checks << Pause::PeriodCheck.new(period_seconds, max_allowed, block_ttl)
|
40
46
|
end
|
41
47
|
|
@@ -51,17 +57,19 @@ module Pause
|
|
51
57
|
@checks = period_checks
|
52
58
|
end
|
53
59
|
|
54
|
-
def increment!(timestamp = Time.now.to_i
|
60
|
+
def increment!(count = 1, timestamp = Time.now.to_i)
|
55
61
|
Pause.analyzer.increment(self, timestamp, count)
|
56
62
|
end
|
57
63
|
|
64
|
+
def rate_limited?
|
65
|
+
! ok?
|
66
|
+
end
|
67
|
+
|
58
68
|
def ok?
|
59
|
-
return true if self.class.disabled?
|
60
69
|
Pause.analyzer.check(self).nil?
|
61
70
|
end
|
62
71
|
|
63
72
|
def analyze
|
64
|
-
return nil if self.class.disabled?
|
65
73
|
Pause.analyzer.check(self)
|
66
74
|
end
|
67
75
|
|
@@ -69,12 +77,12 @@ module Pause
|
|
69
77
|
Pause.analyzer.tracked_identifiers(self.class_scope)
|
70
78
|
end
|
71
79
|
|
72
|
-
def self.
|
73
|
-
Pause.analyzer.
|
80
|
+
def self.rate_limited_identifiers
|
81
|
+
Pause.analyzer.rate_limited_identifiers(self.class_scope)
|
74
82
|
end
|
75
83
|
|
76
84
|
def self.unblock_all
|
77
|
-
Pause.analyzer.adapter.
|
85
|
+
Pause.analyzer.adapter.delete_rate_limited_keys(self.class_scope)
|
78
86
|
end
|
79
87
|
|
80
88
|
def key
|
data/lib/pause/analyzer.rb
CHANGED
@@ -22,8 +22,8 @@ module Pause
|
|
22
22
|
adapter.all_keys(scope)
|
23
23
|
end
|
24
24
|
|
25
|
-
def
|
26
|
-
adapter.
|
25
|
+
def rate_limited_identifiers(scope)
|
26
|
+
adapter.rate_limited_keys(scope)
|
27
27
|
end
|
28
28
|
|
29
29
|
private
|
@@ -37,10 +37,10 @@ module Pause
|
|
37
37
|
break if element.ts < start_time
|
38
38
|
sum += element.count
|
39
39
|
if sum >= period_check.max_allowed
|
40
|
-
adapter.
|
40
|
+
adapter.rate_limit!(action.key, period_check.block_ttl)
|
41
41
|
# Note that Time.now is different from period_marker(resolution, Time.now), which
|
42
42
|
# rounds down to the nearest (resolution) seconds
|
43
|
-
return Pause::
|
43
|
+
return Pause::RateLimitedEvent.new(action, period_check, sum, Time.now.to_i)
|
44
44
|
end
|
45
45
|
sum
|
46
46
|
end
|
data/lib/pause/redis/adapter.rb
CHANGED
@@ -31,27 +31,27 @@ module Pause
|
|
31
31
|
extract_set_elements(white_key(key))
|
32
32
|
end
|
33
33
|
|
34
|
-
def
|
35
|
-
redis.setex(
|
34
|
+
def rate_limit!(key, block_ttl)
|
35
|
+
redis.setex(rate_limited_key(key), block_ttl, nil)
|
36
36
|
end
|
37
37
|
|
38
|
-
def
|
39
|
-
!!redis.get(
|
38
|
+
def rate_limited?(key)
|
39
|
+
!!redis.get(rate_limited_key(key))
|
40
40
|
end
|
41
41
|
|
42
42
|
def all_keys(scope)
|
43
43
|
keys(white_key(scope))
|
44
44
|
end
|
45
45
|
|
46
|
-
def
|
47
|
-
keys(
|
46
|
+
def rate_limited_keys(scope)
|
47
|
+
keys(rate_limited_key(scope))
|
48
48
|
end
|
49
49
|
|
50
|
-
def
|
51
|
-
ids =
|
50
|
+
def delete_rate_limited_keys(scope)
|
51
|
+
ids = rate_limited_keys(scope)
|
52
52
|
increment_keys = ids.map{ |key| white_key(scope, key) }
|
53
|
-
|
54
|
-
redis.del (increment_keys +
|
53
|
+
rate_limited_keys = ids.map{ |key| rate_limited_key(scope, key) }
|
54
|
+
redis.del (increment_keys + rate_limited_keys)
|
55
55
|
end
|
56
56
|
|
57
57
|
def disable(scope)
|
@@ -82,7 +82,7 @@ module Pause
|
|
82
82
|
["i", scope, key].compact.join(':')
|
83
83
|
end
|
84
84
|
|
85
|
-
def
|
85
|
+
def rate_limited_key(scope, key = nil)
|
86
86
|
["b", scope, key].compact.join(':')
|
87
87
|
end
|
88
88
|
|
data/lib/pause/version.rb
CHANGED
data/pause.gemspec
CHANGED
@@ -6,7 +6,7 @@ require 'pause/version'
|
|
6
6
|
Gem::Specification.new do |gem|
|
7
7
|
gem.name = "pause"
|
8
8
|
gem.version = Pause::VERSION
|
9
|
-
gem.authors = ["Atasay Gokkaya", "Paul Henry", "Eric
|
9
|
+
gem.authors = ["Atasay Gokkaya", "Paul Henry", "Eric Saxby"]
|
10
10
|
gem.email = %w(atasay@wanelo.com paul@wanelo.com sax@wanelo.com)
|
11
11
|
gem.description = %q(Real time redis rate limiting)
|
12
12
|
gem.summary = %q(Real time redis rate limiting)
|
data/spec/pause/action_spec.rb
CHANGED
@@ -6,8 +6,8 @@ describe Pause::Action do
|
|
6
6
|
|
7
7
|
class MyNotification < Pause::Action
|
8
8
|
scope "ipn:follow"
|
9
|
-
check 20, 5, 40
|
10
|
-
check 40, 7, 40
|
9
|
+
check period_seconds: 20, max_allowed: 5, block_ttl: 40
|
10
|
+
check period_seconds: 40, max_allowed: 7, block_ttl: 40
|
11
11
|
end
|
12
12
|
|
13
13
|
let(:resolution) { 10 }
|
@@ -47,73 +47,45 @@ describe Pause::Action do
|
|
47
47
|
end
|
48
48
|
|
49
49
|
it "should successfully consider different period checks" do
|
50
|
-
time = period_marker(resolution, Time.now.to_i
|
50
|
+
time = period_marker(resolution, Time.now.to_i)
|
51
51
|
|
52
|
-
|
53
|
-
|
54
|
-
action.increment!
|
55
|
-
action.ok?.should be_true
|
56
|
-
end
|
57
|
-
end
|
58
|
-
|
59
|
-
Timecop.freeze Time.at(time - 5) do
|
60
|
-
2.times do
|
61
|
-
action.increment!
|
62
|
-
action.ok?.should be_true
|
63
|
-
end
|
64
|
-
action.increment!
|
65
|
-
action.ok?.should be_false
|
66
|
-
end
|
67
|
-
end
|
52
|
+
action.increment! 4, time - 25
|
53
|
+
action.ok?.should be_true
|
68
54
|
|
69
|
-
|
55
|
+
action.increment! 2, time - 3
|
56
|
+
action.ok?.should be_true
|
70
57
|
|
71
|
-
|
72
|
-
10.times { action.increment! }
|
73
|
-
action.ok?.should be_false
|
58
|
+
action.increment! 1, time
|
74
59
|
|
75
|
-
|
60
|
+
action.ok?.should be_false
|
76
61
|
|
77
|
-
action.ok?.should be_true
|
78
|
-
end
|
79
62
|
end
|
80
63
|
end
|
81
64
|
|
82
65
|
describe "#analyze" do
|
83
|
-
context "action should not be
|
66
|
+
context "action should not be rate limited" do
|
84
67
|
it "returns nil" do
|
85
68
|
action.analyze.should be_nil
|
86
69
|
end
|
87
70
|
end
|
88
71
|
|
89
|
-
context "action should be
|
90
|
-
it "returns a
|
72
|
+
context "action should be rate limited" do
|
73
|
+
it "returns a RateLimitedEvent object" do
|
91
74
|
time = Time.now
|
92
|
-
|
75
|
+
rate_limit = nil
|
93
76
|
|
94
77
|
Timecop.freeze time do
|
95
78
|
7.times { action.increment! }
|
96
|
-
|
79
|
+
rate_limit = action.analyze
|
97
80
|
end
|
98
81
|
|
99
|
-
|
100
|
-
|
101
|
-
blocked_action.should be_a(Pause::BlockedAction)
|
102
|
-
blocked_action.identifier.should == expected_blocked_action.identifier
|
103
|
-
blocked_action.sum.should == expected_blocked_action.sum
|
104
|
-
blocked_action.period_check.should == expected_blocked_action.period_check
|
105
|
-
blocked_action.timestamp.should == expected_blocked_action.timestamp
|
106
|
-
end
|
107
|
-
end
|
82
|
+
expected_rate_limit = Pause::RateLimitedEvent.new(action, action.checks[0], 7, time.to_i)
|
108
83
|
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
MyNotification.disable
|
115
|
-
|
116
|
-
action.analyze.should be_nil
|
84
|
+
rate_limit.should be_a(Pause::RateLimitedEvent)
|
85
|
+
rate_limit.identifier.should == expected_rate_limit.identifier
|
86
|
+
rate_limit.sum.should == expected_rate_limit.sum
|
87
|
+
rate_limit.period_check.should == expected_rate_limit.period_check
|
88
|
+
rate_limit.timestamp.should == expected_rate_limit.timestamp
|
117
89
|
end
|
118
90
|
end
|
119
91
|
end
|
@@ -131,16 +103,16 @@ describe Pause::Action do
|
|
131
103
|
end
|
132
104
|
end
|
133
105
|
|
134
|
-
describe "#
|
106
|
+
describe "#rate_limited_identifiers" do
|
135
107
|
it "should return all the identifiers blocked" do
|
136
|
-
action.increment!(Time.now.to_i
|
137
|
-
other_action.increment!(Time.now.to_i
|
108
|
+
action.increment!(100, Time.now.to_i)
|
109
|
+
other_action.increment!(100, Time.now.to_i)
|
138
110
|
|
139
111
|
action.ok?
|
140
112
|
other_action.ok?
|
141
113
|
|
142
|
-
MyNotification.
|
143
|
-
MyNotification.
|
114
|
+
MyNotification.rate_limited_identifiers.should include(action.identifier)
|
115
|
+
MyNotification.rate_limited_identifiers.should include(other_action.identifier)
|
144
116
|
end
|
145
117
|
end
|
146
118
|
|
@@ -153,11 +125,11 @@ describe Pause::Action do
|
|
153
125
|
other_action.ok?
|
154
126
|
|
155
127
|
MyNotification.tracked_identifiers.should include(action.identifier, other_action.identifier)
|
156
|
-
MyNotification.
|
128
|
+
MyNotification.rate_limited_identifiers.should == [action.identifier]
|
157
129
|
|
158
130
|
MyNotification.unblock_all
|
159
131
|
|
160
|
-
MyNotification.
|
132
|
+
MyNotification.rate_limited_identifiers.should be_empty
|
161
133
|
MyNotification.tracked_identifiers.should == [other_action.identifier]
|
162
134
|
end
|
163
135
|
end
|
@@ -174,9 +146,13 @@ describe Pause::Action, ".check" do
|
|
174
146
|
check 300, 150, 200
|
175
147
|
end
|
176
148
|
|
149
|
+
class ActionWithHashChecks < Pause::Action
|
150
|
+
check period_seconds: 50, block_ttl: 60, max_allowed: 100
|
151
|
+
end
|
152
|
+
|
177
153
|
it "should define a period check on new instances" do
|
178
154
|
ActionWithCheck.new("id").checks.should == [
|
179
|
-
Pause::PeriodCheck.new(100, 150, 200)
|
155
|
+
Pause::PeriodCheck.new(100, 150, 200)
|
180
156
|
]
|
181
157
|
end
|
182
158
|
|
@@ -188,6 +164,12 @@ describe Pause::Action, ".check" do
|
|
188
164
|
]
|
189
165
|
end
|
190
166
|
|
167
|
+
it "should accept hash arguments" do
|
168
|
+
ActionWithHashChecks.new("id").checks.should == [
|
169
|
+
Pause::PeriodCheck.new(50, 100, 60)
|
170
|
+
]
|
171
|
+
end
|
172
|
+
|
191
173
|
end
|
192
174
|
|
193
175
|
describe Pause::Action, ".scope" do
|
data/spec/pause/analyzer_spec.rb
CHANGED
@@ -37,7 +37,7 @@ describe Pause::Analyzer do
|
|
37
37
|
describe "#analyze" do
|
38
38
|
it "checks and blocks if max_allowed is reached" do
|
39
39
|
time = Time.now
|
40
|
-
adapter.should_receive(:
|
40
|
+
adapter.should_receive(:rate_limit!).once.with(action.key, 12)
|
41
41
|
Timecop.freeze time do
|
42
42
|
5.times do
|
43
43
|
analyzer.increment(action)
|
@@ -57,7 +57,7 @@ describe Pause::Analyzer do
|
|
57
57
|
5.times do
|
58
58
|
analyzer.increment(action)
|
59
59
|
end
|
60
|
-
analyzer.check(action).should be_a(Pause::
|
60
|
+
analyzer.check(action).should be_a(Pause::RateLimitedEvent)
|
61
61
|
end
|
62
62
|
end
|
63
63
|
end
|
@@ -53,7 +53,7 @@ describe Pause::Redis::Adapter do
|
|
53
53
|
let(:ttl) { 110000 }
|
54
54
|
|
55
55
|
it "saves ip to redis with expiration" do
|
56
|
-
adapter.
|
56
|
+
adapter.rate_limit!(key, ttl)
|
57
57
|
redis_conn.get(blocked_key).should_not be_nil
|
58
58
|
redis_conn.ttl(blocked_key).should == ttl
|
59
59
|
end
|
@@ -65,8 +65,8 @@ describe Pause::Redis::Adapter do
|
|
65
65
|
let(:ttl) { 110000 }
|
66
66
|
|
67
67
|
it "should return true if blocked" do
|
68
|
-
adapter.
|
69
|
-
(!!redis_conn.get(blocked_key).should) == adapter.
|
68
|
+
adapter.rate_limit!(key, ttl)
|
69
|
+
(!!redis_conn.get(blocked_key).should) == adapter.rate_limited?(key)
|
70
70
|
end
|
71
71
|
end
|
72
72
|
|
data/spec/spec_helper.rb
CHANGED
@@ -9,8 +9,7 @@ ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
|
|
9
9
|
require 'rubygems'
|
10
10
|
require 'bundler/setup' if File.exists?(ENV['BUNDLE_GEMFILE'])
|
11
11
|
require 'pause'
|
12
|
-
|
13
|
-
Dir['spec/support/**/*.rb'].each { |filename| require_relative "../#{filename}" }
|
12
|
+
require 'support/fakeredis'
|
14
13
|
|
15
14
|
RSpec.configure do |config|
|
16
15
|
config.treat_symbols_as_metadata_keys_with_true_values = true
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: pause
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.4
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
8
8
|
- Atasay Gokkaya
|
9
9
|
- Paul Henry
|
10
|
-
- Eric
|
10
|
+
- Eric Saxby
|
11
11
|
autorequire:
|
12
12
|
bindir: bin
|
13
13
|
cert_chain: []
|
@@ -122,6 +122,7 @@ files:
|
|
122
122
|
- .pairs
|
123
123
|
- .rspec
|
124
124
|
- .rvmrc
|
125
|
+
- .travis.yml
|
125
126
|
- Gemfile
|
126
127
|
- Guardfile
|
127
128
|
- LICENSE.txt
|
@@ -130,9 +131,9 @@ files:
|
|
130
131
|
- lib/pause.rb
|
131
132
|
- lib/pause/action.rb
|
132
133
|
- lib/pause/analyzer.rb
|
133
|
-
- lib/pause/blocked_action.rb
|
134
134
|
- lib/pause/configuration.rb
|
135
135
|
- lib/pause/helper/timing.rb
|
136
|
+
- lib/pause/rate_limited_event.rb
|
136
137
|
- lib/pause/redis/adapter.rb
|
137
138
|
- lib/pause/version.rb
|
138
139
|
- pause.gemspec
|
@@ -173,4 +174,3 @@ test_files:
|
|
173
174
|
- spec/pause/redis/adapter_spec.rb
|
174
175
|
- spec/spec_helper.rb
|
175
176
|
- spec/support/fakeredis.rb
|
176
|
-
has_rdoc:
|