resque-unique 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) Ronny Haryanto, Chris Wanstrath, Ray Krueger
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ Software), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,53 @@
1
+ Resque Unique
2
+ =============
3
+
4
+ This is a modified version of [resque-lock][rl]. This version uses the
5
+ algorithm mentioned in the [Redis SETNX documentation][setnx] to avoid
6
+ deadlocks. This version should also clean up the locks after the worker is
7
+ finished performing, regardless of the outcome.
8
+
9
+ A [Resque][rq] plugin. Requires Resque 1.7.0.
10
+
11
+ If you want only one instance of your job queued at a time, extend it
12
+ with this module.
13
+
14
+
15
+ For example:
16
+
17
+ require 'resque/plugins/unique'
18
+
19
+ class UpdateNetworkGraph
20
+ extend Resque::Plugins::Unique
21
+
22
+ def self.perform(repo_id)
23
+ heavy_lifting
24
+ end
25
+ end
26
+
27
+ While this job is queued or running, no other UpdateNetworkGraph
28
+ jobs with the same arguments will be placed on the queue.
29
+
30
+ If you want to define the key yourself you can override the
31
+ `lock` class method in your subclass, e.g.
32
+
33
+ class UpdateNetworkGraph
34
+ extend Resque::Plugins::Unique
35
+
36
+ Run only one at a time, regardless of repo_id.
37
+ def self.lock(repo_id)
38
+ "network-graph"
39
+ end
40
+
41
+ def self.perform(repo_id)
42
+ heavy_lifting
43
+ end
44
+ end
45
+
46
+ The above modification will ensure only one job of class
47
+ UpdateNetworkGraph is queued at a time, regardless of the
48
+ repo_id. Normally a job is locked using a combination of its
49
+ class name and arguments.
50
+
51
+ [rq]: http://github.com/defunkt/resque
52
+ [rl]: http://github.com/defunkt/resque-lock
53
+ [setnx]: http://redis.io/commands/setnx
data/Rakefile ADDED
@@ -0,0 +1,5 @@
1
+ require 'rspec/core/rake_task'
2
+
3
+ RSpec::Core::RakeTask.new(:spec)
4
+
5
+ task :default => :spec
@@ -0,0 +1,49 @@
1
+ module Resque
2
+ module Plugins
3
+ module Unique
4
+ def lock(*args)
5
+ "unique:#{name}-#{normalised_args(*args).to_s}"
6
+ end
7
+
8
+ def lock_timeout
9
+ @lock_timeout || 3_600 #seconds
10
+ end
11
+
12
+ def before_enqueue_unique(*args)
13
+ acquire_lock(*args)
14
+ end
15
+
16
+ def around_perform_unique(*args)
17
+ begin
18
+ yield
19
+ ensure
20
+ # Always clear the lock when we're done, even if there is an
21
+ # error.
22
+ Resque.redis.del(lock(*args))
23
+ end
24
+ end
25
+
26
+ private
27
+
28
+ def normalised_args(*args)
29
+ args.map {|arg| arg.to_s.to_sym}
30
+ end
31
+
32
+ def lock_expires_at
33
+ ((Time.now.to_f * 1_000).to_i + (lock_timeout * 1_000) + 1_000).to_i
34
+ end
35
+
36
+ # http://redis.io/commands/setnx
37
+ def acquire_lock(*args)
38
+ if Resque.redis.setnx(lock(*args), lock_expires_at)
39
+ true
40
+ else
41
+ new_lock_expiry = lock_expires_at
42
+ existing_lock_expiry = Resque.redis.getset(lock(*args), new_lock_expiry).to_i
43
+ existing_lock_expiry <= Time.now.to_i
44
+ end
45
+ end
46
+
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,14 @@
1
+ require 'rspec/autorun'
2
+ Dir["spec/support/**/*.rb"].each {|f| require f}
3
+
4
+ require 'timecop'
5
+
6
+ RSpec.configure do |config|
7
+ config.before(:each) do
8
+ Resque.redis.flushall
9
+ end
10
+
11
+ config.after(:each) do
12
+ Resque.redis.flushall
13
+ end
14
+ end
@@ -0,0 +1,103 @@
1
+ require 'resque'
2
+ require 'resque/plugins/unique'
3
+
4
+ require 'spec_helper'
5
+
6
+ describe "Resque::Plugins::Unique" do
7
+ let(:queue) { :unique_test }
8
+ let(:resque_queue_key) { "queue:#{queue}" }
9
+ let(:arg) { :success }
10
+ let(:lock) { Job.lock(arg) }
11
+ let(:expires_at) {
12
+ ((Time.now.to_f * 1_000).to_i + 3_600_000 + 1_000).to_i
13
+ }
14
+
15
+
16
+ class Job
17
+ extend Resque::Plugins::Unique
18
+ @queue = :unique_test
19
+
20
+ def self.perform(arg)
21
+ if arg.to_sym == :raise
22
+ raise "perform: simulated exception"
23
+ else
24
+ puts "perform: #{arg}"
25
+ end
26
+ end
27
+ end
28
+
29
+ it "passes Resque::Plugin.lint" do
30
+ expect {
31
+ Resque::Plugin.lint(Resque::Plugins::Unique)
32
+ }.not_to raise_error
33
+ end
34
+
35
+ it "has before_enqueue_hooks" do
36
+ Resque::Plugin.should respond_to :before_enqueue_hooks
37
+ end
38
+
39
+ describe "enqueue" do
40
+ it "ensures only one instance of a job exists" do
41
+ 3.times { Resque.enqueue(Job, arg) }
42
+ Resque.redis.llen(resque_queue_key).should == 1
43
+ end
44
+
45
+ it "locks with time out" do
46
+ Timecop.freeze(Time.now) do
47
+ 3.times { Resque.enqueue(Job, arg) }
48
+ Resque.redis.get(lock).to_i.should == expires_at
49
+ end
50
+ end
51
+
52
+ it "advances lock expiry on subsequent enqueueing" do
53
+ Resque.enqueue(Job, arg)
54
+ first_expiry = Resque.redis.get(lock)
55
+ Timecop.freeze(Time.now + 10) do
56
+ Resque.enqueue(Job, arg)
57
+ second_expiry = Resque.redis.get(lock)
58
+ second_expiry.to_i.should > first_expiry.to_i
59
+ end
60
+ end
61
+ end
62
+
63
+ describe "worker" do
64
+ let(:worker) do
65
+ Resque::Worker.new(queue).tap do |w|
66
+ w.term_child = "1"
67
+ w.very_verbose = "1"
68
+ # w.verbose = "1"
69
+ end
70
+ end
71
+
72
+ before :each do
73
+ Resque.enqueue(Job, arg)
74
+ lock.should_not be_nil
75
+ Resque.redis.get(lock).should_not be_nil
76
+ end
77
+
78
+ shared_examples_for "a sane worker" do
79
+ it "removes the lock" do
80
+ worker.work(0)
81
+ Resque.redis.get(lock).should be_nil
82
+ end
83
+
84
+ it "allows requeueing" do
85
+ worker.work(0)
86
+ Resque.redis.get(lock).should be_nil
87
+ Resque.enqueue(Job, arg)
88
+ lock.should_not be_nil
89
+ Resque.redis.get(lock).should_not be_nil
90
+ end
91
+ end
92
+
93
+ context "when job was successfully performed" do
94
+ let!(:arg) { :success }
95
+ it_behaves_like "a sane worker"
96
+ end
97
+
98
+ context "when job failed" do
99
+ let!(:arg) { :raise }
100
+ it_behaves_like "a sane worker"
101
+ end
102
+ end
103
+ end
metadata ADDED
@@ -0,0 +1,55 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: resque-unique
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Ronny Haryanto
9
+ - Chris Wanstrath
10
+ - Ray Krueger
11
+ autorequire:
12
+ bindir: bin
13
+ cert_chain: []
14
+ date: 2012-09-13 00:00:00.000000000 Z
15
+ dependencies: []
16
+ description: ! "A Resque plugin. If you want only one instance of your job\nqueued
17
+ at a time, extend it with this module.\n\nFor example:\n\n class UpdateNetworkGraph\n
18
+ \ extend Resque::Plugins::Unique\n\n def self.perform(repo_id)\n heavy_lifting\n
19
+ \ end\n end\n"
20
+ email: ronny@haryan.to
21
+ executables: []
22
+ extensions: []
23
+ extra_rdoc_files: []
24
+ files:
25
+ - README.md
26
+ - Rakefile
27
+ - LICENSE
28
+ - lib/resque/plugins/unique.rb
29
+ - spec/spec_helper.rb
30
+ - spec/unique_spec.rb
31
+ homepage: http://github.com/ronny/resque-unique
32
+ licenses: []
33
+ post_install_message:
34
+ rdoc_options: []
35
+ require_paths:
36
+ - lib
37
+ required_ruby_version: !ruby/object:Gem::Requirement
38
+ none: false
39
+ requirements:
40
+ - - ! '>='
41
+ - !ruby/object:Gem::Version
42
+ version: '0'
43
+ required_rubygems_version: !ruby/object:Gem::Requirement
44
+ none: false
45
+ requirements:
46
+ - - ! '>='
47
+ - !ruby/object:Gem::Version
48
+ version: '0'
49
+ requirements: []
50
+ rubyforge_project:
51
+ rubygems_version: 1.8.24
52
+ signing_key:
53
+ specification_version: 3
54
+ summary: Resque plugin to ensure only one instance of a job exists in a queue.
55
+ test_files: []