pause 0.0.3 → 0.0.4

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.
@@ -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: