pause 0.0.3 → 0.0.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,10 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.9.3
4
+ script: "bundle exec rspec"
5
+ notifications:
6
+ email:
7
+ recipients:
8
+ - dev@wanelo.com
9
+ on_success: never
10
+ on_failure: always
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
- Define local actions for your application. These should define a scope, by
42
- which they are identified in the persistent store, and checks.
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
- Checks are configured with the following arguments:
65
+ #### Resolution
45
66
 
46
- * `period_seconds` - this is a period of time against which an action is tested
47
- * `max_allowed` - the maximum number of times an action can be incremented during the time block determined by
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
- Note that you should not configure a check with `period_seconds` less than the minimum resolution set in the
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 "ipn:follow"
59
- check 600, 100, 300
60
- check 3600, 200, 1200
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
- # show errors
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!(Time.now.to_i, params[:count].to_i)
103
+ action.increment!(params[:count].to_i, Time.now.to_i)
85
104
  end
86
105
  end
87
106
  end
@@ -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/blocked_action'
7
+ require 'pause/rate_limited_event'
7
8
 
8
9
  module Pause
9
10
  class PeriodCheck < Struct.new(:period_seconds, :max_allowed, :block_ttl)
@@ -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 - 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
- # ttl - time to mark identifier as not ok
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 10, 20, 30 # period_seconds, max_allowed, ttl
34
- # check 20, 30, 40 # period_seconds, max_allowed, ttl
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(period_seconds, max_allowed, block_ttl)
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, count = 1)
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.blocked_identifiers
73
- Pause.analyzer.blocked_identifiers(self.class_scope)
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.delete_keys(self.class_scope)
85
+ Pause.analyzer.adapter.delete_rate_limited_keys(self.class_scope)
78
86
  end
79
87
 
80
88
  def key
@@ -22,8 +22,8 @@ module Pause
22
22
  adapter.all_keys(scope)
23
23
  end
24
24
 
25
- def blocked_identifiers(scope)
26
- adapter.blocked_keys(scope)
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.block(action.key, period_check.block_ttl)
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::BlockedAction.new(action, period_check, sum, Time.now.to_i)
43
+ return Pause::RateLimitedEvent.new(action, period_check, sum, Time.now.to_i)
44
44
  end
45
45
  sum
46
46
  end
@@ -1,5 +1,5 @@
1
1
  module Pause
2
- class BlockedAction
2
+ class RateLimitedEvent
3
3
  attr_accessor :action, :identifier, :period_check, :sum, :timestamp
4
4
 
5
5
  def initialize(action, period_check, sum, timestamp)
@@ -31,27 +31,27 @@ module Pause
31
31
  extract_set_elements(white_key(key))
32
32
  end
33
33
 
34
- def block(key, block_ttl)
35
- redis.setex(blocked_key(key), block_ttl, nil)
34
+ def rate_limit!(key, block_ttl)
35
+ redis.setex(rate_limited_key(key), block_ttl, nil)
36
36
  end
37
37
 
38
- def blocked?(key)
39
- !!redis.get(blocked_key(key))
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 blocked_keys(scope)
47
- keys(blocked_key(scope))
46
+ def rate_limited_keys(scope)
47
+ keys(rate_limited_key(scope))
48
48
  end
49
49
 
50
- def delete_keys(scope)
51
- ids = blocked_keys(scope)
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
- blocked_keys = ids.map{ |key| blocked_key(scope, key) }
54
- redis.del (increment_keys + blocked_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 blocked_key(scope, key = nil)
85
+ def rate_limited_key(scope, key = nil)
86
86
  ["b", scope, key].compact.join(':')
87
87
  end
88
88
 
@@ -1,3 +1,3 @@
1
1
  module Pause
2
- VERSION = "0.0.3"
2
+ VERSION = "0.0.4"
3
3
  end
@@ -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 Saxny"]
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)
@@ -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 + 1)
50
+ time = period_marker(resolution, Time.now.to_i)
51
51
 
52
- Timecop.freeze Time.at(time - 35) do
53
- 4.times do
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
- context "action is disabled" do
55
+ action.increment! 2, time - 3
56
+ action.ok?.should be_true
70
57
 
71
- it "should be true if action is disabled, even if blocked" do
72
- 10.times { action.increment! }
73
- action.ok?.should be_false
58
+ action.increment! 1, time
74
59
 
75
- MyNotification.disable
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 blocked" do
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 blocked" do
90
- it "returns a BlockedAction object" do
72
+ context "action should be rate limited" do
73
+ it "returns a RateLimitedEvent object" do
91
74
  time = Time.now
92
- blocked_action = nil
75
+ rate_limit = nil
93
76
 
94
77
  Timecop.freeze time do
95
78
  7.times { action.increment! }
96
- blocked_action = action.analyze
79
+ rate_limit = action.analyze
97
80
  end
98
81
 
99
- expected_blocked_action = Pause::BlockedAction.new(action, action.checks[0], 7, time.to_i)
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
- context "action is disabled" do
110
- it "return nil, even if blocked" do
111
- 10.times { action.increment! }
112
- action.should_not be_ok
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 "#blocked_identifiers" do
106
+ describe "#rate_limited_identifiers" do
135
107
  it "should return all the identifiers blocked" do
136
- action.increment!(Time.now.to_i, 100)
137
- other_action.increment!(Time.now.to_i, 100)
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.blocked_identifiers.should include(action.identifier)
143
- MyNotification.blocked_identifiers.should include(other_action.identifier)
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.blocked_identifiers.should == [action.identifier]
128
+ MyNotification.rate_limited_identifiers.should == [action.identifier]
157
129
 
158
130
  MyNotification.unblock_all
159
131
 
160
- MyNotification.blocked_identifiers.should be_empty
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
@@ -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(:block).once.with(action.key, 12)
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::BlockedAction)
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.block(key, ttl)
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.block(key, ttl)
69
- (!!redis_conn.get(blocked_key).should) == adapter.blocked?(key)
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
 
@@ -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.3
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 Saxny
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: