pause 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,18 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ .idea
data/.pairs ADDED
@@ -0,0 +1,13 @@
1
+ pairs:
2
+ ag: Atasay Gokkaya; atasay
3
+ km: Kaan Meralan; kaan
4
+ kg: Konstantin Gredeskoul; kig
5
+ ph: Paul Henry; paul
6
+ sf: Sean Flannagan; sean
7
+ es: Eric Saxby; sax
8
+ tn: Truong Nguyen; constantx
9
+ cc: Cihan Cimen; cihan
10
+ sc: Server Cimen; server
11
+ email:
12
+ prefix: pair
13
+ domain: wanelo.com
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format progress
data/.rvmrc ADDED
@@ -0,0 +1 @@
1
+ rvm use 1.9.3@pause --create
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in pause.gemspec
4
+ gemspec
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env ruby
2
+ #^syntax detection
3
+
4
+ # A sample Guardfile
5
+ # More info at https://github.com/guard/guard#readme
6
+
7
+ guard 'rspec' do
8
+ watch(%r{^spanx\.gemspec}) { "spec"}
9
+ watch(%r{^spec/.+_spec\.rb$})
10
+ watch(%r{^lib/(.+)\.rb$}) { "spec" }
11
+ watch('spec/spec_helper.rb') { "spec" }
12
+ end
13
+
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 TODO: Write your name
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,117 @@
1
+ # Pause
2
+
3
+ Pause is a redis-backed rate-limiting client. Use it to track events, with
4
+ rules around how often they are allowed to occur within configured time checks.
5
+
6
+ ## Installation
7
+
8
+ Add this line to your application's Gemfile:
9
+
10
+ gem 'pause'
11
+
12
+ And then execute:
13
+
14
+ $ bundle
15
+
16
+ Or install it yourself as:
17
+
18
+ $ gem install pause
19
+
20
+ ## Usage
21
+
22
+ Configure Pause. This could be in a Rails initializer.
23
+
24
+ * resolution - The time resolution (in seconds) defining the minimum period into which action counts are
25
+ aggregated. This defines the size of the persistent store. The higher the number, the less data needs
26
+ to be persisted in Redis.
27
+ * history - The maximum amount of time (in seconds) that data is persisted
28
+
29
+ ```ruby
30
+ Pause.configure do |config|
31
+ config.redis_host = "127.0.0.1"
32
+ config.redis_port = 6379
33
+ config.redis_db = 1
34
+
35
+ config.resolution = 600
36
+ config.history = 86400
37
+ end
38
+ ```
39
+
40
+ Define local actions for your application. These should define a scope, by
41
+ which they are identified in the persistent store, and checks.
42
+
43
+ Checks are configured with the following arguments:
44
+
45
+ * `period_seconds` - this is a period of time against which an action is tested
46
+ * `max_allowed` - the maximum number of times an action can be incremented during the time block determined by
47
+ period seconds
48
+ * `block_ttl` - how long to mark an action as blocked if it goes over max-allowed
49
+
50
+ Note that you should not configure a check with `period_seconds` less than the minimum resolution set in the
51
+ Pause config. If you do so, you will actually be checking sums against the full time period.
52
+
53
+ ```ruby
54
+ require 'pause'
55
+
56
+ class FollowAction < Pause::Action
57
+ scope "ipn:follow"
58
+ check 600, 100, 300
59
+ check 3600, 200, 1200
60
+ end
61
+ ```
62
+
63
+ When an event occurs, you increment an instance of your action, optionally with a timestamp and count. This saves
64
+ data into a redis store, so it can be checked later by other processes. Timestamps should be in unix epoch format.
65
+
66
+ ```ruby
67
+ class FollowsController < ApplicationController
68
+ def create
69
+ action = FollowAction.new(user.id)
70
+ if action.ok?
71
+ # do stuff
72
+ action.increment!
73
+ else
74
+ # show errors
75
+ end
76
+ end
77
+ end
78
+
79
+ class OtherController < ApplicationController
80
+ def index
81
+ action = OtherAction.new(params[:thing])
82
+ if action.ok?
83
+ action.increment!(Time.now.to_i, params[:count].to_i)
84
+ end
85
+ end
86
+ end
87
+ ```
88
+
89
+ If more data is needed about why the action is blocked, the `analyze` can be called
90
+
91
+ ```ruby
92
+ action = MyAction.new("thing")
93
+
94
+ while true
95
+ action.increment!
96
+
97
+ blocked_action = action.analyze
98
+
99
+ if blocked_action
100
+ puts blocked_action.identifier
101
+ puts blocked_action.sum
102
+ puts blocked_action.timestamp
103
+
104
+ puts blocked_aciton.period_check.inspect
105
+ end
106
+
107
+ sleep 1
108
+ end
109
+ ```
110
+
111
+ ## Contributing
112
+
113
+ 1. Fork it
114
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
115
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
116
+ 4. Push to the branch (`git push origin my-new-feature`)
117
+ 5. Create new Pull Request
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,34 @@
1
+ require "pause/version"
2
+ require "pause/configuration"
3
+ require "pause/action"
4
+ require "pause/analyzer"
5
+ require "pause/redis/adapter"
6
+ require 'pause/blocked_action'
7
+
8
+ module Pause
9
+ class PeriodCheck < Struct.new(:period_seconds, :max_allowed, :block_ttl)
10
+ def <=>(other)
11
+ self.period_seconds <=> other.period_seconds
12
+ end
13
+ end
14
+
15
+ class SetElement < Struct.new(:ts, :count)
16
+ def <=>(other)
17
+ self.ts <=> other.ts
18
+ end
19
+ end
20
+
21
+ class << self
22
+ def analyzer
23
+ @analyzer ||= Pause::Analyzer.new
24
+ end
25
+
26
+ def configure(&block)
27
+ @configuration = Pause::Configuration.new.configure(&block)
28
+ end
29
+
30
+ def config
31
+ @configuration
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,84 @@
1
+ module Pause
2
+ class Action
3
+ attr_accessor :identifier
4
+
5
+ def initialize(identifier)
6
+ @identifier = identifier
7
+ self.class.checks = [] unless self.class.instance_variable_get(:@checks)
8
+ end
9
+
10
+ # Action subclasses should define their scope as follows
11
+ #
12
+ # class MyAction < Pause::Action
13
+ # scope "my:scope"
14
+ # end
15
+ #
16
+ def scope
17
+ raise "Should implement scope. (Ex: ipn:follow)"
18
+ end
19
+
20
+ def self.scope(scope_identifier = nil)
21
+ class_variable_set(:@@class_scope, scope_identifier)
22
+ define_method(:scope) { scope_identifier }
23
+ end
24
+
25
+ # Action subclasses should define their checks as follows
26
+ #
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
31
+ #
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
35
+ # end
36
+ #
37
+ def self.check(period_seconds, max_allowed, block_ttl)
38
+ @checks ||= []
39
+ @checks << Pause::PeriodCheck.new(period_seconds, max_allowed, block_ttl)
40
+ end
41
+
42
+ def checks
43
+ self.class.instance_variable_get(:@checks)
44
+ end
45
+
46
+ def self.checks=(period_checks)
47
+ @checks = period_checks
48
+ end
49
+
50
+ def increment!(timestamp = Time.now.to_i, count = 1)
51
+ Pause.analyzer.increment(self, timestamp, count)
52
+ end
53
+
54
+ def ok?
55
+ Pause.analyzer.check(self).nil?
56
+ end
57
+
58
+ def analyze
59
+ Pause.analyzer.check(self)
60
+ end
61
+
62
+ def self.tracked_identifiers
63
+ Pause.analyzer.tracked_identifiers(self.class_scope)
64
+ end
65
+
66
+ def self.blocked_identifiers
67
+ Pause.analyzer.blocked_identifiers(self.class_scope)
68
+ end
69
+
70
+ def self.unblock_all
71
+ Pause.analyzer.adapter.delete_keys(self.class_scope)
72
+ end
73
+
74
+ def key
75
+ "#{self.scope}:#{@identifier}"
76
+ end
77
+
78
+ private
79
+
80
+ def self.class_scope
81
+ class_variable_get:@@class_scope if class_variable_defined?(:@@class_scope)
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,52 @@
1
+ require 'pause/helper/timing'
2
+
3
+ module Pause
4
+ class Analyzer
5
+ include Pause::Helper::Timing
6
+
7
+ attr_accessor :adapter
8
+
9
+ def initialize
10
+ @adapter ||= Pause::Redis::Adapter.new(Pause.config)
11
+ end
12
+
13
+ def increment(action, timestamp = Time.now.to_i, count = 1)
14
+ adapter.increment(action.key, timestamp, count)
15
+ end
16
+
17
+ def check(action)
18
+ analyze(action)
19
+ end
20
+
21
+ def tracked_identifiers(scope)
22
+ adapter.all_keys(scope)
23
+ end
24
+
25
+ def blocked_identifiers(scope)
26
+ adapter.blocked_keys(scope)
27
+ end
28
+
29
+ private
30
+
31
+ def analyze(action)
32
+ timestamp = period_marker(Pause.config.resolution, Time.now.to_i)
33
+ set = adapter.key_history(action.key)
34
+ action.checks.each do |period_check|
35
+ start_time = timestamp - period_check.period_seconds
36
+ set.reverse.inject(0) do |sum, element|
37
+ break if element.ts < start_time
38
+ sum += element.count
39
+ if sum >= period_check.max_allowed
40
+ adapter.block(action.key, period_check.block_ttl)
41
+ # Note that Time.now is different from period_marker(resolution, Time.now), which
42
+ # rounds down to the nearest (resolution) seconds
43
+ return Pause::BlockedAction.new(action, period_check, sum, Time.now.to_i)
44
+ end
45
+ sum
46
+ end
47
+ end
48
+ nil
49
+ end
50
+
51
+ end
52
+ end
@@ -0,0 +1,14 @@
1
+ module Pause
2
+ class BlockedAction
3
+ attr_accessor :action, :identifier, :period_check, :sum, :timestamp
4
+
5
+ def initialize(action, period_check, sum, timestamp)
6
+ @action = action
7
+ @identifier = action.identifier
8
+ @period_check = period_check
9
+ @sum = sum
10
+ @timestamp = timestamp
11
+ end
12
+
13
+ end
14
+ end
@@ -0,0 +1,30 @@
1
+ module Pause
2
+ class Configuration
3
+ attr_writer :redis_host, :redis_port, :redis_db, :resolution, :history
4
+
5
+ def configure
6
+ yield self
7
+ self
8
+ end
9
+
10
+ def redis_host
11
+ @redis_host || "127.0.0.1"
12
+ end
13
+
14
+ def redis_port
15
+ (@redis_port || 6379).to_i
16
+ end
17
+
18
+ def redis_db
19
+ @redis_db || '1'
20
+ end
21
+
22
+ def resolution
23
+ (@resolution || 600).to_i
24
+ end
25
+
26
+ def history
27
+ (@history || 86400).to_i
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,9 @@
1
+ module Pause
2
+ module Helper
3
+ module Timing
4
+ def period_marker(resolution, timestamp = Time.now)
5
+ timestamp.to_i / resolution * resolution
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,86 @@
1
+ require 'pause/helper/timing'
2
+
3
+ module Pause
4
+ module Redis
5
+ class Adapter
6
+
7
+ include Pause::Helper::Timing
8
+ attr_accessor :resolution, :time_blocks_to_keep, :history
9
+
10
+ def initialize(config)
11
+ @resolution = config.resolution
12
+ @time_blocks_to_keep = config.history / @resolution
13
+ @history = config.history
14
+ end
15
+
16
+ def increment(key, timestamp, count = 1)
17
+ k = white_key(key)
18
+ redis.multi do |redis|
19
+ redis.zincrby k, count, period_marker(resolution, timestamp)
20
+ redis.expire k, history
21
+ end
22
+
23
+ if redis.zcard(k) > time_blocks_to_keep
24
+ list = extract_set_elements(k)
25
+ to_remove = list.slice(0, (list.size - time_blocks_to_keep))
26
+ redis.zrem(k, to_remove.map(&:ts))
27
+ end
28
+ end
29
+
30
+ def key_history(key)
31
+ extract_set_elements(white_key(key))
32
+ end
33
+
34
+ def block(key, block_ttl)
35
+ redis.setex(blocked_key(key), block_ttl, nil)
36
+ end
37
+
38
+ def blocked?(key)
39
+ !!redis.get(blocked_key(key))
40
+ end
41
+
42
+ def all_keys(scope)
43
+ keys(white_key(scope))
44
+ end
45
+
46
+ def blocked_keys(scope)
47
+ keys(blocked_key(scope))
48
+ end
49
+
50
+ def delete_keys(scope)
51
+ ids = blocked_keys(scope)
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)
55
+ end
56
+
57
+ private
58
+
59
+ def redis
60
+ @redis_conn ||= ::Redis.new(host: Pause.config.redis_host,
61
+ port: Pause.config.redis_port,
62
+ db: Pause.config.redis_db)
63
+ end
64
+
65
+ def white_key(scope, key = nil)
66
+ ["i", scope, key].compact.join(':')
67
+ end
68
+
69
+ def blocked_key(scope, key = nil)
70
+ ["b", scope, key].compact.join(':')
71
+ end
72
+
73
+ def keys(key_scope)
74
+ redis.keys("#{key_scope}:*").map do |key|
75
+ key.gsub(/^#{key_scope}:/, "")
76
+ end
77
+ end
78
+
79
+ def extract_set_elements(key)
80
+ (redis.zrange key, 0, -1, :with_scores => true).map do |slice|
81
+ Pause::SetElement.new(slice[0].to_i, slice[1].to_i)
82
+ end.sort
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,3 @@
1
+ module Pause
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,27 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'pause/version'
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.name = "pause"
8
+ gem.version = Pause::VERSION
9
+ gem.authors = ["Atasay Gokkaya", "Paul Henry"]
10
+ gem.email = %w(atasay@wanelo.com paul@wanelo.com)
11
+ gem.description = %q(Real time redis rate limiting)
12
+ gem.summary = %q(Real time redis rate limiting)
13
+ gem.homepage = "https://github.com/wanelo/pause"
14
+
15
+ gem.files = `git ls-files`.split($/)
16
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
17
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
18
+ gem.require_paths = ["lib"]
19
+
20
+ gem.add_dependency 'redis'
21
+
22
+ gem.add_development_dependency 'rspec'
23
+ gem.add_development_dependency 'fakeredis'
24
+ gem.add_development_dependency 'timecop'
25
+ gem.add_development_dependency 'guard-rspec'
26
+ gem.add_development_dependency 'rb-fsevent'
27
+ end
@@ -0,0 +1,185 @@
1
+ require 'spec_helper'
2
+ require 'timecop'
3
+
4
+ describe Pause::Action do
5
+ include Pause::Helper::Timing
6
+
7
+ class MyNotification < Pause::Action
8
+ scope "ipn:follow"
9
+ check 20, 5, 40
10
+ check 40, 7, 40
11
+ end
12
+
13
+ let(:resolution) { 10 }
14
+ let(:history) { 60 }
15
+ let(:configuration) { Pause::Configuration.new }
16
+
17
+ before do
18
+ Pause.stub(:config).and_return(configuration)
19
+ Pause.config.stub(:resolution).and_return(resolution)
20
+ Pause.config.stub(:history).and_return(history)
21
+ end
22
+
23
+ let(:action) { MyNotification.new("1237612") }
24
+ let(:other_action) { MyNotification.new("1237613") }
25
+
26
+ describe "#increment!" do
27
+ it "should increment" do
28
+ time = Time.now
29
+ Timecop.freeze time do
30
+ Pause.analyzer.should_receive(:increment).with(action, time.to_i, 1)
31
+ action.increment!
32
+ end
33
+ end
34
+ end
35
+
36
+ describe "#ok?" do
37
+ it "should successfully return if the action is blocked or not" do
38
+ time = Time.now
39
+ Timecop.freeze time do
40
+ 4.times do
41
+ action.increment!
42
+ action.ok?.should be_true
43
+ end
44
+ action.increment!
45
+ action.ok?.should be_false
46
+ end
47
+ end
48
+
49
+ it "should successfully consider different period checks" do
50
+ time = Time.now
51
+ Timecop.freeze time do
52
+ 4.times do
53
+ action.increment!
54
+ action.ok?.should be_true
55
+ end
56
+ end
57
+ Timecop.freeze Time.at(time.to_i + 30) do
58
+ 2.times do
59
+ action.increment!
60
+ action.ok?.should be_true
61
+ end
62
+ action.increment!
63
+ action.ok?.should be_false
64
+ end
65
+ end
66
+ end
67
+
68
+ describe "#analyze" do
69
+ context "action should not be blocked" do
70
+ it "returns nil" do
71
+ action.analyze.should be_nil
72
+ end
73
+ end
74
+
75
+ context "action should be blocked" do
76
+ it "returns a BlockedAction object" do
77
+ time = Time.now
78
+ blocked_action = nil
79
+
80
+ Timecop.freeze time do
81
+ 7.times { action.increment! }
82
+ blocked_action = action.analyze
83
+ end
84
+
85
+ expected_blocked_action = Pause::BlockedAction.new(action, action.checks[0], 7, time.to_i)
86
+
87
+ blocked_action.should be_a(Pause::BlockedAction)
88
+ blocked_action.identifier.should == expected_blocked_action.identifier
89
+ blocked_action.sum.should == expected_blocked_action.sum
90
+ blocked_action.period_check.should == expected_blocked_action.period_check
91
+ blocked_action.timestamp.should == expected_blocked_action.timestamp
92
+ end
93
+ end
94
+ end
95
+
96
+ describe "#tracked_identifiers" do
97
+ it "should return all the identifiers tracked (but not blocked) so far" do
98
+ action.increment!
99
+ other_action.increment!
100
+
101
+ action.ok?
102
+ other_action.ok?
103
+
104
+ MyNotification.tracked_identifiers.should include(action.identifier)
105
+ MyNotification.tracked_identifiers.should include(other_action.identifier)
106
+ end
107
+ end
108
+
109
+ describe "#blocked_identifiers" do
110
+ it "should return all the identifiers blocked" do
111
+ action.increment!(Time.now.to_i, 100)
112
+ other_action.increment!(Time.now.to_i, 100)
113
+
114
+ action.ok?
115
+ other_action.ok?
116
+
117
+ MyNotification.blocked_identifiers.should include(action.identifier)
118
+ MyNotification.blocked_identifiers.should include(other_action.identifier)
119
+ end
120
+ end
121
+
122
+ describe "#unblock_all" do
123
+ it "should unblock all the identifiers for a scope" do
124
+ 10.times { action.increment! }
125
+ other_action.increment!
126
+
127
+ action.ok?
128
+ other_action.ok?
129
+
130
+ MyNotification.tracked_identifiers.should include(action.identifier, other_action.identifier)
131
+ MyNotification.blocked_identifiers.should == [action.identifier]
132
+
133
+ MyNotification.unblock_all
134
+
135
+ MyNotification.blocked_identifiers.should be_empty
136
+ MyNotification.tracked_identifiers.should == [other_action.identifier]
137
+ end
138
+ end
139
+ end
140
+
141
+ describe Pause::Action, ".check" do
142
+ class ActionWithCheck < Pause::Action
143
+ check 100, 150, 200
144
+ end
145
+
146
+ class ActionWithMultipleChecks < Pause::Action
147
+ check 100, 150, 200
148
+ check 200, 150, 200
149
+ check 300, 150, 200
150
+ end
151
+
152
+ it "should define a period check on new instances" do
153
+ ActionWithCheck.new("id").checks.should == [
154
+ Pause::PeriodCheck.new(100, 150, 200),
155
+ ]
156
+ end
157
+
158
+ it "should define a period check on new instances" do
159
+ ActionWithMultipleChecks.new("id").checks.should == [
160
+ Pause::PeriodCheck.new(100, 150, 200),
161
+ Pause::PeriodCheck.new(200, 150, 200),
162
+ Pause::PeriodCheck.new(300, 150, 200)
163
+ ]
164
+ end
165
+
166
+ end
167
+
168
+ describe Pause::Action, ".scope" do
169
+ class UndefinedScopeAction < Pause::Action
170
+ end
171
+
172
+ it "should raise if scope is not defined" do
173
+ lambda {
174
+ UndefinedScopeAction.new("1.2.3.4").scope
175
+ }.should raise_error("Should implement scope. (Ex: ipn:follow)")
176
+ end
177
+
178
+ class DefinedScopeAction < Pause::Action
179
+ scope "my:scope"
180
+ end
181
+
182
+ it "should set scope on class" do
183
+ DefinedScopeAction.new("1.2.3.4").scope.should == "my:scope"
184
+ end
185
+ end
@@ -0,0 +1,64 @@
1
+ require 'spec_helper'
2
+ require 'timecop'
3
+
4
+ describe Pause::Analyzer do
5
+ include Pause::Helper::Timing
6
+
7
+ class FollowPushNotification < Pause::Action
8
+ scope "ipn:follow"
9
+ check 20, 5, 12
10
+ check 40, 7, 12
11
+ end
12
+
13
+ let(:resolution) { 10 }
14
+ let(:history) { 60 }
15
+ let(:configuration) { Pause::Configuration.new }
16
+
17
+ before do
18
+ Pause.stub(:config).and_return(configuration)
19
+ Pause.config.stub(:resolution).and_return(resolution)
20
+ Pause.config.stub(:history).and_return(history)
21
+ end
22
+
23
+ let(:analyzer) { Pause.analyzer }
24
+ let(:adapter) { Pause.analyzer.adapter }
25
+ let(:action) { FollowPushNotification.new("1243123") }
26
+
27
+ describe "#increment" do
28
+ it "should increment an action" do
29
+ time = Time.now
30
+ adapter.should_receive(:increment).with(action.key, time.to_i, 1)
31
+ Timecop.freeze time do
32
+ analyzer.increment(action)
33
+ end
34
+ end
35
+ end
36
+
37
+ describe "#analyze" do
38
+ it "checks and blocks if max_allowed is reached" do
39
+ time = Time.now
40
+ adapter.should_receive(:block).once.with(action.key, 12)
41
+ Timecop.freeze time do
42
+ 5.times do
43
+ analyzer.increment(action)
44
+ analyzer.check(action)
45
+ end
46
+ end
47
+ end
48
+ end
49
+
50
+ describe "#check" do
51
+ it "should return nil if action is NOT blocked" do
52
+ analyzer.check(action).should be_nil
53
+ end
54
+
55
+ it "should return blocked action if action is blocked" do
56
+ Timecop.freeze Time.now do
57
+ 5.times do
58
+ analyzer.increment(action)
59
+ end
60
+ analyzer.check(action).should be_a(Pause::BlockedAction)
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,36 @@
1
+ require 'spec_helper'
2
+
3
+ describe Pause::Configuration, "#configure" do
4
+
5
+ subject { Pause::Configuration.new }
6
+
7
+ it "should allow configuration via block" do
8
+ subject.configure do |c|
9
+ c.redis_host = "128.23.12.8"
10
+ c.redis_port = "2134"
11
+ c.redis_db = "13"
12
+
13
+ c.resolution = 5000
14
+ c.history = 6000
15
+ end
16
+
17
+ subject.redis_host.should == "128.23.12.8"
18
+ subject.redis_port.should == 2134
19
+ subject.redis_db.should == "13"
20
+
21
+ subject.resolution.should == 5000
22
+ subject.history.should == 6000
23
+ end
24
+
25
+ it "should provide redis defaults" do
26
+ subject.configure do |config|
27
+ # do nothing
28
+ end
29
+
30
+ subject.redis_host.should == "127.0.0.1"
31
+ subject.redis_port.should == 6379
32
+ subject.redis_db.should == "1"
33
+ subject.resolution.should == 600 # 10 minutes
34
+ subject.history.should == 86400 # one day
35
+ end
36
+ end
@@ -0,0 +1,78 @@
1
+ require 'spec_helper'
2
+ require 'date'
3
+ require 'timecop'
4
+
5
+ describe Pause::Redis::Adapter do
6
+
7
+ let(:resolution) { 10 }
8
+ let(:history) { 60 }
9
+ let(:configuration) { Pause::Configuration.new }
10
+
11
+ before do
12
+ Pause.stub(:config).and_return(configuration)
13
+ Pause.config.stub(:resolution).and_return(resolution)
14
+ Pause.config.stub(:history).and_return(history)
15
+ end
16
+
17
+ let(:adapter) { Pause::Redis::Adapter.new(Pause.config) }
18
+ let(:redis_conn) { adapter.send(:redis) }
19
+
20
+ describe '#increment' do
21
+ let(:key) { "213213" }
22
+
23
+ it "should add key to a redis set" do
24
+ adapter.increment(key, Time.now.to_i)
25
+ set = redis_conn.zrange(adapter.send(:white_key, key), 0, -1, :with_scores => true)
26
+ set.should_not be_empty
27
+ set.size.should eql(1)
28
+ set[0].size.should eql(2)
29
+ end
30
+
31
+ it "should remove old key from a redis set" do
32
+ time = Time.now
33
+ redis_conn.should_receive(:zrem).with(adapter.send(:white_key, key), [adapter.period_marker(resolution, time)])
34
+
35
+ adapter.time_blocks_to_keep = 1
36
+ Timecop.freeze time do
37
+ adapter.increment(key, Time.now.to_i)
38
+ end
39
+ Timecop.freeze time + (adapter.resolution + 1) do
40
+ adapter.increment(key, Time.now.to_i)
41
+ end
42
+ end
43
+
44
+ it "sets expiry on key" do
45
+ redis_conn.should_receive(:expire).with(adapter.send(:white_key, key), history)
46
+ adapter.increment(key, Time.now.to_i)
47
+ end
48
+ end
49
+
50
+ describe "#block" do
51
+ let(:key) { "ipn:follow:123461234" }
52
+ let(:blocked_key) { "b:#{key}" }
53
+ let(:ttl) { 110000 }
54
+
55
+ it "saves ip to redis with expiration" do
56
+ adapter.block(key, ttl)
57
+ redis_conn.get(blocked_key).should_not be_nil
58
+ redis_conn.ttl(blocked_key).should == ttl
59
+ end
60
+ end
61
+
62
+ describe "#blocked?" do
63
+ let(:key) { "ipn:follow:123461234" }
64
+ let(:blocked_key) { "b:#{key}" }
65
+ let(:ttl) { 110000 }
66
+
67
+ it "should return true if blocked" do
68
+ adapter.block(key, ttl)
69
+ (!!redis_conn.get(blocked_key).should) == adapter.blocked?(key)
70
+ end
71
+ end
72
+
73
+ describe "#white_key" do
74
+ it "prefixes key" do
75
+ adapter.send(:white_key, "abc").should == "i:abc"
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,25 @@
1
+ # This file was generated by the `rspec --init` command. Conventionally, all
2
+ # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
3
+ # Require this file using `require "spec_helper"` to ensure that it is only
4
+ # loaded once.
5
+ #
6
+ # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
7
+
8
+ ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
9
+ require 'rubygems'
10
+ require 'bundler/setup' if File.exists?(ENV['BUNDLE_GEMFILE'])
11
+ require 'pause'
12
+
13
+ Dir['spec/support/**/*.rb'].each { |filename| require_relative "../#{filename}" }
14
+
15
+ RSpec.configure do |config|
16
+ config.treat_symbols_as_metadata_keys_with_true_values = true
17
+ config.run_all_when_everything_filtered = true
18
+ config.filter_run :focus
19
+
20
+ # Run specs in random order to surface order dependencies. If you find an
21
+ # order dependency and want to debug it, you can fix the order by providing
22
+ # the seed, which is printed after each run.
23
+ # --seed 1234
24
+ config.order = 'random'
25
+ end
@@ -0,0 +1 @@
1
+ require 'fakeredis/rspec'
metadata ADDED
@@ -0,0 +1,174 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: pause
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Atasay Gokkaya
9
+ - Paul Henry
10
+ autorequire:
11
+ bindir: bin
12
+ cert_chain: []
13
+ date: 2012-11-13 00:00:00.000000000 Z
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: redis
17
+ requirement: !ruby/object:Gem::Requirement
18
+ none: false
19
+ requirements:
20
+ - - ! '>='
21
+ - !ruby/object:Gem::Version
22
+ version: '0'
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ none: false
27
+ requirements:
28
+ - - ! '>='
29
+ - !ruby/object:Gem::Version
30
+ version: '0'
31
+ - !ruby/object:Gem::Dependency
32
+ name: rspec
33
+ requirement: !ruby/object:Gem::Requirement
34
+ none: false
35
+ requirements:
36
+ - - ! '>='
37
+ - !ruby/object:Gem::Version
38
+ version: '0'
39
+ type: :development
40
+ prerelease: false
41
+ version_requirements: !ruby/object:Gem::Requirement
42
+ none: false
43
+ requirements:
44
+ - - ! '>='
45
+ - !ruby/object:Gem::Version
46
+ version: '0'
47
+ - !ruby/object:Gem::Dependency
48
+ name: fakeredis
49
+ requirement: !ruby/object:Gem::Requirement
50
+ none: false
51
+ requirements:
52
+ - - ! '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ type: :development
56
+ prerelease: false
57
+ version_requirements: !ruby/object:Gem::Requirement
58
+ none: false
59
+ requirements:
60
+ - - ! '>='
61
+ - !ruby/object:Gem::Version
62
+ version: '0'
63
+ - !ruby/object:Gem::Dependency
64
+ name: timecop
65
+ requirement: !ruby/object:Gem::Requirement
66
+ none: false
67
+ requirements:
68
+ - - ! '>='
69
+ - !ruby/object:Gem::Version
70
+ version: '0'
71
+ type: :development
72
+ prerelease: false
73
+ version_requirements: !ruby/object:Gem::Requirement
74
+ none: false
75
+ requirements:
76
+ - - ! '>='
77
+ - !ruby/object:Gem::Version
78
+ version: '0'
79
+ - !ruby/object:Gem::Dependency
80
+ name: guard-rspec
81
+ requirement: !ruby/object:Gem::Requirement
82
+ none: false
83
+ requirements:
84
+ - - ! '>='
85
+ - !ruby/object:Gem::Version
86
+ version: '0'
87
+ type: :development
88
+ prerelease: false
89
+ version_requirements: !ruby/object:Gem::Requirement
90
+ none: false
91
+ requirements:
92
+ - - ! '>='
93
+ - !ruby/object:Gem::Version
94
+ version: '0'
95
+ - !ruby/object:Gem::Dependency
96
+ name: rb-fsevent
97
+ requirement: !ruby/object:Gem::Requirement
98
+ none: false
99
+ requirements:
100
+ - - ! '>='
101
+ - !ruby/object:Gem::Version
102
+ version: '0'
103
+ type: :development
104
+ prerelease: false
105
+ version_requirements: !ruby/object:Gem::Requirement
106
+ none: false
107
+ requirements:
108
+ - - ! '>='
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ description: Real time redis rate limiting
112
+ email:
113
+ - atasay@wanelo.com
114
+ - paul@wanelo.com
115
+ executables: []
116
+ extensions: []
117
+ extra_rdoc_files: []
118
+ files:
119
+ - .gitignore
120
+ - .pairs
121
+ - .rspec
122
+ - .rvmrc
123
+ - Gemfile
124
+ - Guardfile
125
+ - LICENSE.txt
126
+ - README.md
127
+ - Rakefile
128
+ - lib/pause.rb
129
+ - lib/pause/action.rb
130
+ - lib/pause/analyzer.rb
131
+ - lib/pause/blocked_action.rb
132
+ - lib/pause/configuration.rb
133
+ - lib/pause/helper/timing.rb
134
+ - lib/pause/redis/adapter.rb
135
+ - lib/pause/version.rb
136
+ - pause.gemspec
137
+ - spec/pause/action_spec.rb
138
+ - spec/pause/analyzer_spec.rb
139
+ - spec/pause/configuration_spec.rb
140
+ - spec/pause/redis/adapter_spec.rb
141
+ - spec/spec_helper.rb
142
+ - spec/support/fakeredis.rb
143
+ homepage: https://github.com/wanelo/pause
144
+ licenses: []
145
+ post_install_message:
146
+ rdoc_options: []
147
+ require_paths:
148
+ - lib
149
+ required_ruby_version: !ruby/object:Gem::Requirement
150
+ none: false
151
+ requirements:
152
+ - - ! '>='
153
+ - !ruby/object:Gem::Version
154
+ version: '0'
155
+ required_rubygems_version: !ruby/object:Gem::Requirement
156
+ none: false
157
+ requirements:
158
+ - - ! '>='
159
+ - !ruby/object:Gem::Version
160
+ version: '0'
161
+ requirements: []
162
+ rubyforge_project:
163
+ rubygems_version: 1.8.24
164
+ signing_key:
165
+ specification_version: 3
166
+ summary: Real time redis rate limiting
167
+ test_files:
168
+ - spec/pause/action_spec.rb
169
+ - spec/pause/analyzer_spec.rb
170
+ - spec/pause/configuration_spec.rb
171
+ - spec/pause/redis/adapter_spec.rb
172
+ - spec/spec_helper.rb
173
+ - spec/support/fakeredis.rb
174
+ has_rdoc: