pause 0.0.1

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,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: