resque-restriction 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore CHANGED
@@ -1 +1,2 @@
1
1
  spec/dump.rdb
2
+ pkg/**
data/README.markdown CHANGED
@@ -9,18 +9,23 @@ Resque Restriction is a plugin for the [Resque][0] queueing system (http://githu
9
9
 
10
10
  Resque Restriction requires Resque 1.7.0.
11
11
 
12
+ Install
13
+ -------
14
+
15
+ sudo gem install resque-restriction
16
+
12
17
  To use
13
18
  ------
14
19
 
15
- The job you wish to restrict should inherit from Resque::Plugins::RestrictionJob class and add restrict definition. Example:
20
+ It is especially useful when a system has an email invitation resque job, because sending emails too frequentyly will be treated as a spam. What you should do for the InvitationJob is to inherit it from Resque::Plugins::RestrictionJob class and add restrict definition. Example:
16
21
 
17
- class MyRestrictionJob < Resque::Plugins::RestrictionJob
18
- restrict :per_day => 1000, :per_hour => 1000, :per_300 => 30
22
+ class InvitationJob < Resque::Plugins::RestrictionJob
23
+ restrict :per_day => 1000, :per_hour => 100, :per_300 => 30
19
24
 
20
25
  #rest of your class here
21
26
  end
22
27
 
23
- MyRestrictionJob can be run 1000 times per day, 100 times per hour and 30 times per 300 seconds.
28
+ That means the InvitationJob can not be executed more than 1000 times per day, 100 times per hour and 30 times per 300 seconds.
24
29
 
25
30
  The argument of restrict method is a hash, the key of the hash is a period time, including :per_minute, :per_hour, :per_day, :per_week, :per_month, :per_year, and you can also define any period like :per_300 means per 300 seconds. the value of the hash is the job execution limit number in a period.
26
31
 
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.1.0
1
+ 0.2.0
@@ -4,8 +4,8 @@ module Resque
4
4
  alias_method :origin_reserve, :reserve
5
5
 
6
6
  def reserve(queue)
7
- if queue == 'restriction' && payload = Resque.peek(queue)
8
- constantize(payload['class']).repush
7
+ if queue == 'restriction' && payload = Resque.pop(queue)
8
+ constantize(payload['class']).repush(*payload['args'])
9
9
  return
10
10
  end
11
11
  origin_reserve(queue)
@@ -1,6 +1,6 @@
1
1
  module Resque
2
2
  module Plugins
3
- class RestrictionJob
3
+ module Restriction
4
4
  SECONDS = {
5
5
  :per_minute => 60,
6
6
  :per_hour => 60*60,
@@ -10,77 +10,92 @@ module Resque
10
10
  :per_year => 366*24*60*60
11
11
  }
12
12
 
13
- class <<self
14
- def settings
15
- @options ||= {}
16
- end
13
+ def settings
14
+ @options ||= {}
15
+ end
17
16
 
18
- def restrict(options={})
19
- settings.merge!(options)
20
- end
21
-
22
- def before_perform_restriction(*args)
23
- settings.each do |period, number|
24
- key = redis_key(period)
25
- value = get_restrict(key)
17
+ def restrict(options={})
18
+ settings.merge!(options)
19
+ end
20
+
21
+ def before_perform_restriction(*args)
22
+ keys_decremented = []
23
+ settings.each do |period, number|
24
+ key = redis_key(period, *args)
25
+
26
+ # first try to set period key to be the total allowed for the period
27
+ # if we get a 0 result back, the key wasn't set, so we know we are
28
+ # already tracking the count for that period'
29
+ period_active = ! Resque.redis.setnx(key, number.to_i - 1)
26
30
 
27
- if value.nil? or value == ""
28
- set_restrict(key, seconds(period), number)
29
- elsif value.to_i <= 0
31
+ # If we are already tracking that period, then decrement by one to
32
+ # see if we are allowed to run, pushing to restriction queue to run
33
+ # later if not. Note that the value stored is the number of outstanding
34
+ # jobs allowed, thus we need to reincrement if the decr discovers that
35
+ # we have bypassed the limit
36
+ if period_active
37
+ value = Resque.redis.decrby(key, 1).to_i
38
+ keys_decremented << key
39
+ if value < 0
40
+ # reincrement the keys if one of the periods triggers DontPerform so
41
+ # that we accurately track capacity
42
+ keys_decremented.each {|k| Resque.redis.incrby(k, 1) }
30
43
  Resque.push "restriction", :class => to_s, :args => args
31
44
  raise Resque::Job::DontPerform
32
45
  end
33
46
  end
34
47
  end
35
-
36
- def after_perform_restriction(*args)
37
- settings.each do |period, number|
38
- key = redis_key(period)
39
- Resque.redis.decrby(key, 1)
40
- end
41
- end
48
+ end
42
49
 
43
- def redis_key(period)
44
- period_str = case period
45
- when :per_minute, :per_hour, :per_day, :per_week then (Time.now.to_i / SECONDS[period]).to_s
46
- when :per_month then Date.today.strftime("%Y-%m")
47
- when :per_year then Date.today.year.to_s
48
- else period.to_s =~ /^per_(\d+)$/ and (Time.now.to_i / $1.to_i).to_s end
49
- [self.to_s, period_str].compact.join(":")
50
+ def after_perform_restriction(*args)
51
+ if settings[:concurrent]
52
+ key = redis_key(:concurrent, *args)
53
+ Resque.redis.incrby(key, 1)
50
54
  end
55
+ end
51
56
 
52
- def seconds(period)
53
- if SECONDS.keys.include? period
54
- SECONDS[period]
55
- else
56
- period.to_s =~ /^per_(\d+)$/ and $1
57
- end
57
+ def redis_key(period, *args)
58
+ period_str = case period
59
+ when :concurrent then "*"
60
+ when :per_minute, :per_hour, :per_day, :per_week then (Time.now.to_i / SECONDS[period]).to_s
61
+ when :per_month then Date.today.strftime("%Y-%m")
62
+ when :per_year then Date.today.year.to_s
63
+ else period.to_s =~ /^per_(\d+)$/ and (Time.now.to_i / $1.to_i).to_s end
64
+ [self.identifier(*args), period_str].compact.join(":")
65
+ end
66
+
67
+ def identifier(*args)
68
+ self.to_s
69
+ end
70
+
71
+ def seconds(period)
72
+ if SECONDS.keys.include? period
73
+ SECONDS[period]
74
+ else
75
+ period.to_s =~ /^per_(\d+)$/ and $1
58
76
  end
77
+ end
59
78
 
60
- def repush
61
- settings.each do |period, number|
62
- key = redis_key(period)
63
- value = get_restrict(key)
64
- queue_name = Resque.queue_from_class(self)
65
- if value.nil? or value == ""
66
- Resque.redis.rpoplpush('queue:restriction', "queue:#{queue_name}")
67
- end
68
- end
79
+ def repush(*args)
80
+ no_restrictions = true
81
+ queue_name = Resque.queue_from_class(self)
82
+ settings.each do |period, number|
83
+ key = redis_key(period, *args)
84
+ value = Resque.redis.get(key)
85
+ no_restrictions &&= (value.nil? or value == "" or value.to_i > 0)
86
+ end
87
+ if no_restrictions
88
+ Resque.push queue_name, :class => to_s, :args => args
89
+ else
90
+ Resque.push "restriction", :class => to_s, :args => args
69
91
  end
92
+ end
70
93
 
71
- private
72
- # after operation incrby - expire, then decrby will reset the value to 0 first
73
- # use operation set - expire - incrby instead
74
- def set_restrict(key, seconds, number)
75
- Resque.redis.set(key, '')
76
- Resque.redis.expire(key, seconds)
77
- Resque.redis.incrby(key, number)
78
- end
94
+ end
79
95
 
80
- def get_restrict(key)
81
- Resque.redis.get(key)
82
- end
83
- end
96
+ class RestrictionJob
97
+ extend Restriction
84
98
  end
99
+
85
100
  end
86
101
  end
@@ -0,0 +1,61 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = %q{resque-restriction}
8
+ s.version = "0.2.0"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["Richard Huang"]
12
+ s.date = %q{2010-06-29}
13
+ s.description = %q{resque-restriction is an extension to resque queue system that restricts the execution number of certain jobs in a period time, the exceeded jobs will be executed at the next period.}
14
+ s.email = %q{flyerhzm@gmail.com}
15
+ s.extra_rdoc_files = [
16
+ "LICENSE",
17
+ "README.markdown"
18
+ ]
19
+ s.files = [
20
+ ".gitignore",
21
+ "LICENSE",
22
+ "README.markdown",
23
+ "Rakefile",
24
+ "VERSION",
25
+ "init.rb",
26
+ "lib/resque-restriction.rb",
27
+ "lib/resque-restriction/job.rb",
28
+ "lib/resque-restriction/restriction_job.rb",
29
+ "rails/init.rb",
30
+ "resque-restriction.gemspec",
31
+ "spec/redis-test.conf",
32
+ "spec/resque-restriction/job_spec.rb",
33
+ "spec/resque-restriction/restriction_job_spec.rb",
34
+ "spec/spec.opts",
35
+ "spec/spec_helper.rb"
36
+ ]
37
+ s.homepage = %q{http://github.com/flyerhzm/resque-restriction}
38
+ s.rdoc_options = ["--charset=UTF-8"]
39
+ s.require_paths = ["lib"]
40
+ s.rubygems_version = %q{1.3.6}
41
+ s.summary = %q{resque-restriction is an extension to resque queue system that restricts the execution number of certain jobs in a period time.}
42
+ s.test_files = [
43
+ "spec/resque-restriction/job_spec.rb",
44
+ "spec/resque-restriction/restriction_job_spec.rb",
45
+ "spec/spec_helper.rb"
46
+ ]
47
+
48
+ if s.respond_to? :specification_version then
49
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
50
+ s.specification_version = 3
51
+
52
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
53
+ s.add_runtime_dependency(%q<resque>, [">= 1.7.0"])
54
+ else
55
+ s.add_dependency(%q<resque>, [">= 1.7.0"])
56
+ end
57
+ else
58
+ s.add_dependency(%q<resque>, [">= 1.7.0"])
59
+ end
60
+ end
61
+
@@ -1,17 +1,26 @@
1
1
  require File.join(File.dirname(__FILE__) + '/../spec_helper')
2
2
 
3
3
  describe Resque::Job do
4
- it "should repush restrictioin queue when reserve" do
4
+ it "should repush restriction queue when reserve" do
5
5
  Resque.redis.flushall
6
- Resque.push('restriction', :class => 'OneHourRestrictionJob', :args => 'any args')
6
+ Resque.push('restriction', :class => 'OneHourRestrictionJob', :args => ['any args'])
7
7
  Resque::Job.reserve('restriction').should be_nil
8
- Resque::Job.reserve('normal').should == Resque::Job.new('normal', {'class' => 'OneHourRestrictionJob', 'args' => 'any args'})
8
+ Resque::Job.reserve('normal').should == Resque::Job.new('normal', {'class' => 'OneHourRestrictionJob', 'args' => ['any args']})
9
+ Resque::Job.reserve('normal').should be_nil
10
+ end
11
+
12
+ it "should push back to restriction queue when still restricted" do
13
+ Resque.redis.flushall
14
+ Resque.redis.set(OneHourRestrictionJob.redis_key(:per_hour), -1)
15
+ Resque.push('restriction', :class => 'OneHourRestrictionJob', :args => ['any args'])
16
+ Resque::Job.reserve('restriction').should be_nil
17
+ Resque.pop('restriction').should == {'class' => 'OneHourRestrictionJob', 'args' => ['any args']}
9
18
  Resque::Job.reserve('normal').should be_nil
10
19
  end
11
20
 
12
21
  it "should not repush when reserve normal queue" do
13
- Resque.push('normal', :class => 'OneHourRestrictionJob', :args => 'any args')
14
- Resque::Job.reserve('normal').should == Resque::Job.new('normal', {'class' => 'OneHourRestrictionJob', 'args' => 'any args'})
22
+ Resque.push('normal', :class => 'OneHourRestrictionJob', :args => ['any args'])
23
+ Resque::Job.reserve('normal').should == Resque::Job.new('normal', {'class' => 'OneHourRestrictionJob', 'args' => ['any args']})
15
24
  Resque::Job.reserve('normal').should be_nil
16
25
  end
17
26
  end
@@ -42,6 +42,17 @@ describe Resque::Plugins::RestrictionJob do
42
42
  Resque.redis.get(OneHourRestrictionJob.redis_key(:per_hour)).should == "9"
43
43
  end
44
44
 
45
+ it "should use identifier to set exclusive execution counts" do
46
+ result = perform_job(IdentifiedRestrictionJob, 1)
47
+ result.should be_true
48
+ result = perform_job(IdentifiedRestrictionJob, 1)
49
+ result.should be_true
50
+ result = perform_job(IdentifiedRestrictionJob, 2)
51
+ result.should be_true
52
+ Resque.redis.get(IdentifiedRestrictionJob.redis_key(:per_hour, 1)).should == "8"
53
+ Resque.redis.get(IdentifiedRestrictionJob.redis_key(:per_hour, 2)).should == "9"
54
+ end
55
+
45
56
  it "should decrement execution number when one job executed" do
46
57
  Resque.redis.set(OneHourRestrictionJob.redis_key(:per_hour), 6)
47
58
  result = perform_job(OneHourRestrictionJob, "any args")
@@ -49,7 +60,18 @@ describe Resque::Plugins::RestrictionJob do
49
60
  Resque.redis.get(OneHourRestrictionJob.redis_key(:per_hour)).should == "5"
50
61
  end
51
62
 
52
- it "should put the job into restriction queue when execution count <= 0" do
63
+ it "should increment execution number when concurrent job completes" do
64
+ t = Thread.new do
65
+ result = perform_job(ConcurrentRestrictionJob, "any args")
66
+ result.should be_true
67
+ end
68
+ sleep 0.1
69
+ Resque.redis.get(ConcurrentRestrictionJob.redis_key(:concurrent)).should == "0"
70
+ t.join
71
+ Resque.redis.get(ConcurrentRestrictionJob.redis_key(:concurrent)).should == "1"
72
+ end
73
+
74
+ it "should put the job into restriction queue when execution count < 0" do
53
75
  Resque.redis.set(OneHourRestrictionJob.redis_key(:per_hour), 0)
54
76
  result = perform_job(OneHourRestrictionJob, "any args")
55
77
  result.should_not be_true
data/spec/spec_helper.rb CHANGED
@@ -28,7 +28,7 @@ at_exit do
28
28
  exit_code = Spec::Runner.run
29
29
 
30
30
  pid = `ps -e -o pid,command | grep [r]edis-test`.split(" ")[0]
31
- puts "Killing test redis server..."
31
+ puts "Killing test redis server [#{pid}]..."
32
32
  `rm -f #{dir}/dump.rdb`
33
33
  Process.kill("KILL", pid.to_i)
34
34
  exit exit_code
@@ -66,6 +66,29 @@ class OneHourRestrictionJob < Resque::Plugins::RestrictionJob
66
66
  end
67
67
  end
68
68
 
69
+ class IdentifiedRestrictionJob < Resque::Plugins::RestrictionJob
70
+ restrict :per_hour => 10
71
+
72
+ @queue = 'normal'
73
+
74
+ def self.identifier(*args)
75
+ [self.to_s, args.first].join(":")
76
+ end
77
+
78
+ def self.perform(*args)
79
+ end
80
+ end
81
+
82
+ class ConcurrentRestrictionJob < Resque::Plugins::RestrictionJob
83
+ restrict :concurrent => 1
84
+
85
+ @queue = 'normal'
86
+
87
+ def self.perform(*args)
88
+ sleep 0.2
89
+ end
90
+ end
91
+
69
92
  class MultipleRestrictionJob < Resque::Plugins::RestrictionJob
70
93
  restrict :per_hour => 10, :per_300 => 2
71
94
 
metadata CHANGED
@@ -1,7 +1,12 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: resque-restriction
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ prerelease: false
5
+ segments:
6
+ - 0
7
+ - 2
8
+ - 0
9
+ version: 0.2.0
5
10
  platform: ruby
6
11
  authors:
7
12
  - Richard Huang
@@ -9,19 +14,23 @@ autorequire:
9
14
  bindir: bin
10
15
  cert_chain: []
11
16
 
12
- date: 2010-04-30 00:00:00 -06:00
17
+ date: 2010-06-29 00:00:00 +08:00
13
18
  default_executable:
14
19
  dependencies:
15
20
  - !ruby/object:Gem::Dependency
16
21
  name: resque
17
- type: :runtime
18
- version_requirement:
19
- version_requirements: !ruby/object:Gem::Requirement
22
+ prerelease: false
23
+ requirement: &id001 !ruby/object:Gem::Requirement
20
24
  requirements:
21
25
  - - ">="
22
26
  - !ruby/object:Gem::Version
27
+ segments:
28
+ - 1
29
+ - 7
30
+ - 0
23
31
  version: 1.7.0
24
- version:
32
+ type: :runtime
33
+ version_requirements: *id001
25
34
  description: resque-restriction is an extension to resque queue system that restricts the execution number of certain jobs in a period time, the exceeded jobs will be executed at the next period.
26
35
  email: flyerhzm@gmail.com
27
36
  executables: []
@@ -42,6 +51,7 @@ files:
42
51
  - lib/resque-restriction/job.rb
43
52
  - lib/resque-restriction/restriction_job.rb
44
53
  - rails/init.rb
54
+ - resque-restriction.gemspec
45
55
  - spec/redis-test.conf
46
56
  - spec/resque-restriction/job_spec.rb
47
57
  - spec/resque-restriction/restriction_job_spec.rb
@@ -60,18 +70,20 @@ required_ruby_version: !ruby/object:Gem::Requirement
60
70
  requirements:
61
71
  - - ">="
62
72
  - !ruby/object:Gem::Version
73
+ segments:
74
+ - 0
63
75
  version: "0"
64
- version:
65
76
  required_rubygems_version: !ruby/object:Gem::Requirement
66
77
  requirements:
67
78
  - - ">="
68
79
  - !ruby/object:Gem::Version
80
+ segments:
81
+ - 0
69
82
  version: "0"
70
- version:
71
83
  requirements: []
72
84
 
73
85
  rubyforge_project:
74
- rubygems_version: 1.3.5
86
+ rubygems_version: 1.3.6
75
87
  signing_key:
76
88
  specification_version: 3
77
89
  summary: resque-restriction is an extension to resque queue system that restricts the execution number of certain jobs in a period time.