resque-unique-job 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE ADDED
@@ -0,0 +1,19 @@
1
+ Copyright (c) 2011 Engine Yard
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in
11
+ all copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ THE SOFTWARE.
@@ -0,0 +1,15 @@
1
+ resque-unique-job
2
+ ===============
3
+ Depends on [Resque](http://github.com/defunkt/resque/) 1.8
4
+
5
+ About
6
+ -----
7
+
8
+
9
+ Examples
10
+ --------
11
+
12
+
13
+ Requirements
14
+ ------------
15
+ * [resque](http://github.com/defunkt/resque/) 1.8
@@ -0,0 +1 @@
1
+ #TODO
@@ -0,0 +1,15 @@
1
+ Resque.class_eval do
2
+
3
+ def self.dequeue_with_unique_job(klass, *args)
4
+ klass.destroy_matching_keys(queue_from_class(klass), args)
5
+ dequeue_without_unique_job(klass, *args)
6
+ end
7
+
8
+
9
+ class << self
10
+ alias_method :dequeue_without_unique_job, :dequeue
11
+ alias_method :dequeue, :dequeue_with_unique_job
12
+ end
13
+
14
+
15
+ end
@@ -0,0 +1,255 @@
1
+ require 'resque/plugins/resque_ext/destroy'
2
+
3
+ module Resque
4
+ module Plugins
5
+ module UniqueJob
6
+ include Resque::Helpers
7
+
8
+ def enqueue(*args)
9
+ key = unique_redis_key(args)
10
+ ttl = Resque.redis.ttl(key)
11
+ # if it's already enQ'd
12
+ if Resque.redis.getset(key, "1")
13
+ if expire = unique_redis_expiration
14
+ # reset existing expiration
15
+ Resque.redis.expire(key, ttl)
16
+ end
17
+ else
18
+ # set new expiration
19
+ if expire = unique_redis_expiration
20
+ Resque.redis.expire(key, expire)
21
+ end
22
+ super
23
+ end
24
+ end
25
+
26
+ def before_perform_unique_job(*args)
27
+ Resque.redis.del(unique_redis_key(args))
28
+ end
29
+
30
+ def unique_key(args)
31
+ Digest::MD5.hexdigest(encode(:class => self.to_s, :args => args))
32
+ end
33
+
34
+ def destroy_matching_keys(queue, args)
35
+ queue = "queue:#{queue}"
36
+
37
+ klass = self.to_s
38
+ if args.empty?
39
+ redis.lrange(queue, 0, -1).each do |string|
40
+ if decode(string)['class'] == klass
41
+ key = unique_redis_key(decode(string)['args'])
42
+ Resque.redis.del(key)
43
+ end
44
+ end
45
+ else
46
+ Resque.redis.del(unique_redis_key(args))
47
+ end
48
+ end
49
+
50
+ private
51
+
52
+ def unique_redis_expiration
53
+ 600
54
+ end
55
+
56
+ def unique_redis_key(args)
57
+ job_unique_key = unique_key(args)
58
+ "plugin:unique_job:#{job_unique_key}"
59
+ end
60
+ # def self.extended(mod)
61
+ # mod.extend(Resque::Plugins::Meta)
62
+ # mod.extend(Resque::Plugins::Lock)
63
+ # end
64
+ #
65
+ # class Step
66
+ # def initialize(args, run_last = false, &block)
67
+ # @run_last = run_last
68
+ # @signature = args.map{|a| a.to_s}.join(" ")
69
+ # if args.size == 1
70
+ # #no inputs or output
71
+ # @inputs = []
72
+ # elsif args.size >= 2
73
+ # unless @run_last
74
+ # @output = args.pop.to_s
75
+ # end
76
+ # @inputs = []
77
+ # args.each_with_index do |a, index|
78
+ # if index % 2 == 1
79
+ # @inputs << a.to_s
80
+ # end
81
+ # end
82
+ # else
83
+ # raise ArgumentError, "invalid arguments #{args.inspect}"
84
+ # end
85
+ # @block = block
86
+ # end
87
+ # attr_reader :block, :signature, :inputs, :output, :run_last
88
+ # # attr_accessor :run_last
89
+ # # attr_reader :step_name, :what_it_makes, :block
90
+ # def run(available_inputs)
91
+ # begin
92
+ # block_args = @inputs.map do |input_name|
93
+ # available_inputs[input_name]
94
+ # end
95
+ # @block.call(*block_args)
96
+ # rescue Exception => e
97
+ # puts e.inspect
98
+ # puts e.backtrace.join("\n")
99
+ # raise e
100
+ # end
101
+ # end
102
+ # end
103
+ #
104
+ # #TODO:
105
+ # #
106
+ # # Need a way to edit meta data BEFORE a job is enQ'd
107
+ # # so that we can we sure that data is available on dQ
108
+ # #
109
+ # # Need a way to "Lock" editing of the meta data on a job
110
+ # # so other editors of job data must wait before editing job
111
+ # # (2 jobs marking a step done and then, therefore re-enQ-ing the parent)
112
+ # #
113
+ # # need a way to mark a job as already enQ'd (and not yet running)
114
+ # # so that if the tomato just re-enQ'd the sandwich, the cheese will see it on the Q and let it be
115
+ # #
116
+ # # basically, there should be a lock such that if there are any child jobs still enQ'd for a job
117
+ # # it should not run but prioritize the child jobs first
118
+ #
119
+ # class StepDependency
120
+ # def initialize(job_class, args)
121
+ # @job_class = job_class
122
+ # @job_args = args
123
+ # end
124
+ # attr_reader :job_class, :job_args
125
+ # end
126
+ #
127
+ # class Retry
128
+ # def initialize(seconds)
129
+ # @seconds = seconds
130
+ # end
131
+ # attr_reader :seconds
132
+ # end
133
+ #
134
+ # def run_steps(meta_id, *args)
135
+ # @step_list = []
136
+ # @meta = self.get_jobdata(meta_id)
137
+ # # puts "I have meta of: " + @meta.inspect
138
+ #
139
+ # #implicitly builds the @step_list
140
+ # steps(*args)
141
+ #
142
+ # #TODO: raise error if there are duplicate steps defined?
143
+ #
144
+ # # require 'pp'
145
+ # # pp @step_list
146
+ #
147
+ # #figure out which step we are on from meta data
148
+ # @meta["step_count"] ||= @step_list.size
149
+ # steps_ran = @meta["steps_ran"] ||= []
150
+ # steps_running = @meta["steps_running"] ||= []
151
+ # # puts "my steps_ran are #{steps_ran.inspect}"
152
+ # # @step_list.map{ |step| step.signature }
153
+ # available_inputs = @meta["available_inputs"] ||= {}
154
+ # # puts "my available_inputs are #{available_inputs.inspect}"
155
+ #
156
+ # #run last step if no more steps are needed
157
+ # @step_list.each do |step|
158
+ # if steps_ran.include?(step.signature)
159
+ # #already ran
160
+ # elsif steps_running.include?(step.signature)
161
+ # #already Q'd
162
+ # elsif step.run_last
163
+ # # puts "Can't run last step #{step.signature} yet"
164
+ # #this is the last step, only run if all other steps are run
165
+ # elsif (step.inputs - available_inputs.keys).empty?
166
+ # #all of the steps needed inputs are available
167
+ # #run!
168
+ # result = step.run(available_inputs)
169
+ # puts "running step #{step.signature}"
170
+ # if result.is_a?(Retry)
171
+ # puts "enqueue #{self} in #{result.seconds}"
172
+ # Resque.enqueue_in(result.seconds, self, meta_id, *args)
173
+ # elsif result.is_a?(StepDependency)
174
+ # #TODO: what if the child job is dQ'd before caller has a chance to set parent_job
175
+ # # don't re-enQ a child that's already enQ'd!
176
+ # # it might not be in steps ran but it doesn't need to be duplicated!
177
+ # puts "enqueue #{result.job_class}"
178
+ # child_job = result.job_class.enqueue(*result.job_args)
179
+ # @meta["steps_running"] << step.signature
180
+ # child_job["parent_job"] = [self, meta_id, args]
181
+ # child_job["expected_output"] = step.output
182
+ # child_job["signature_from_parent"] = step.signature
183
+ # child_job.save
184
+ # else
185
+ # if step.output
186
+ # available_inputs[step.output] = result
187
+ # end
188
+ # # puts "available_inputs are now #{available_inputs.inspect}"
189
+ # if @meta["steps_ran"].include?(step.signature)
190
+ # raise "WHAT? ran #{step.signature} twice!"
191
+ # end
192
+ # @meta["steps_ran"] << step.signature
193
+ # end
194
+ # else
195
+ # # puts "waiting before we can run step #{step.signature} -- need #{step.inputs}"
196
+ # end
197
+ # end
198
+ #
199
+ # if steps_ran.size + 1 == @step_list.size
200
+ # puts "now running last step of #{self} -- already ran #{steps_ran.inspect}"
201
+ #
202
+ # step = @step_list.last
203
+ # result = step.run(available_inputs)
204
+ # if @meta["parent_job"]
205
+ # # puts "#{meta_id} has parent"
206
+ # parent_job_class_name, parent_meta_id, parent_args = @meta["parent_job"]
207
+ # parent_job_class = const_get(parent_job_class_name)
208
+ # parent_meta = parent_job_class.get_jobdata(parent_meta_id)
209
+ # if expected_output = @meta["expected_output"]
210
+ # parent_meta["available_inputs"][expected_output] = result
211
+ # parent_meta.save
212
+ # end
213
+ # if @meta["signature_from_parent"]
214
+ # if parent_meta["steps_ran"].include?(@meta["signature_from_parent"])
215
+ # raise "WHAT? ran #{@meta["signature_from_parent"]} twice!"
216
+ # end
217
+ # parent_meta["steps_ran"] << @meta["signature_from_parent"]
218
+ # parent_meta.save
219
+ # end
220
+ # puts "enqueue #{parent_job_class}"
221
+ # Resque.enqueue(parent_job_class, parent_meta_id, *parent_args)
222
+ # end
223
+ # if @meta["steps_ran"].include?(step.signature)
224
+ # raise "WHAT? ran #{step.signature} twice!"
225
+ # end
226
+ # @meta["steps_ran"] << step.signature
227
+ # end
228
+ # @meta.save
229
+ #
230
+ # puts "End of #{self}"
231
+ # end
232
+ #
233
+ # def step(*args, &block)
234
+ # @step_list << Step.new(args, &block)
235
+ # end
236
+ #
237
+ # def last_step(*args, &block)
238
+ # @step_list << Step.new(args, true, &block)
239
+ # end
240
+ #
241
+ # def depend_on(job_class, *args)
242
+ # StepDependency.new(job_class, args)
243
+ # end
244
+ #
245
+ # def retry_in(seconds)
246
+ # Retry.new(seconds)
247
+ # end
248
+ #
249
+ # def get_jobdata(meta_id)
250
+ # get_meta(meta_id)
251
+ # end
252
+
253
+ end
254
+ end
255
+ end
@@ -0,0 +1,7 @@
1
+ module Resque
2
+ module Plugins
3
+ module UniqueJob
4
+ Version = '0.0.1'
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,149 @@
1
+ require 'resque'
2
+ require 'resque/plugins/unique_job'
3
+
4
+ class WhatHappened
5
+ require 'tempfile'
6
+ def self.reset!
7
+ @what_happened = Tempfile.new("what_happened")
8
+ end
9
+ def self.what_happened
10
+ File.read(@what_happened.path)
11
+ end
12
+ def self.record(*event)
13
+ @what_happened.write(event.to_s)
14
+ @what_happened.flush
15
+ end
16
+ end
17
+
18
+ class BaseJob
19
+
20
+ def self.enqueue(*args)
21
+ Resque.enqueue(self, *args)
22
+ end
23
+
24
+ def self.perform(*args)
25
+ begin
26
+ WhatHappened.record(self, args)
27
+ rescue => e
28
+ puts e.inspect
29
+ puts e.backtrace.join("\n")
30
+ end
31
+ end
32
+
33
+ end
34
+
35
+ class BasicJob < BaseJob
36
+ extend Resque::Plugins::UniqueJob
37
+ @queue = :test
38
+
39
+ end
40
+
41
+ class DifferentJob < BaseJob
42
+ extend Resque::Plugins::UniqueJob
43
+ @queue = :test
44
+
45
+ end
46
+
47
+ class NotUniqueJob < BaseJob
48
+ @queue = :test
49
+ end
50
+
51
+ class ShortExpiringJob < BaseJob
52
+ extend Resque::Plugins::UniqueJob
53
+ @queue = :test
54
+
55
+ def self.unique_redis_expiration
56
+ 1
57
+ end
58
+ end
59
+
60
+
61
+ describe Resque::Plugins::UniqueJob do
62
+ before(:each) do
63
+ WhatHappened.reset!
64
+ Resque.redis.flushall
65
+ end
66
+
67
+ it "works for 1 job" do
68
+ BasicJob.enqueue("foo", "bar")
69
+ worker = Resque::Worker.new(:test)
70
+ worker.work(0)
71
+ WhatHappened.what_happened.should == "BasicJobfoobar"
72
+ end
73
+
74
+ describe "uniqueness" do
75
+
76
+ it "cares about job class" do
77
+ BasicJob.enqueue("foo", "bar")
78
+ DifferentJob.enqueue("foo", "bar")
79
+ worker = Resque::Worker.new(:test)
80
+ worker.work(0)
81
+ WhatHappened.what_happened.should == "BasicJobfoobarDifferentJobfoobar"
82
+ end
83
+
84
+ it "only enqueues the job once" do
85
+ BasicJob.enqueue("foo", "bar")
86
+ BasicJob.enqueue("foo", "bar")
87
+ worker = Resque::Worker.new(:test)
88
+ worker.work(0)
89
+ WhatHappened.what_happened.should == "BasicJobfoobar"
90
+ end
91
+
92
+ it "re-enqueues after the job has been processed" do
93
+ worker = Resque::Worker.new(:test)
94
+ BasicJob.enqueue("foo", "bar")
95
+ worker.work(0)
96
+ BasicJob.enqueue("foo", "bar")
97
+ worker.work(0)
98
+ WhatHappened.what_happened.should == "BasicJobfoobarBasicJobfoobar"
99
+ end
100
+
101
+ it "doesn't make non unique jobs unique" do
102
+ NotUniqueJob.enqueue("foo", "bar")
103
+ NotUniqueJob.enqueue("foo", "bar")
104
+ worker = Resque::Worker.new(:test)
105
+ worker.work(0)
106
+ WhatHappened.what_happened.should == "NotUniqueJobfoobarNotUniqueJobfoobar"
107
+ end
108
+
109
+ it "respects dequeue" do
110
+ BasicJob.enqueue("foo", "bar")
111
+ Resque.dequeue(BasicJob, "foo", "bar")
112
+ BasicJob.enqueue("foo", "bar")
113
+ worker = Resque::Worker.new(:test)
114
+ worker.work(0)
115
+ WhatHappened.what_happened.should == "BasicJobfoobar"
116
+ end
117
+
118
+ it "respects destroy dequeue all" do
119
+ BasicJob.enqueue("foo", "bar")
120
+ BasicJob.enqueue("bar", "baz")
121
+ Resque.dequeue(BasicJob)
122
+ BasicJob.enqueue("foo", "bar")
123
+ worker = Resque::Worker.new(:test)
124
+ worker.work(0)
125
+ WhatHappened.what_happened.should == "BasicJobfoobar"
126
+ end
127
+
128
+ it "doesn't destroy too much" do
129
+ BasicJob.enqueue("foo", "bar")
130
+ DifferentJob.enqueue("foo", "bar")
131
+ Resque.dequeue(BasicJob, "foo", "bar")
132
+ # DifferentJob.enqueue("foo", "bar")
133
+ worker = Resque::Worker.new(:test)
134
+ worker.work(0)
135
+ WhatHappened.what_happened.should == "DifferentJobfoobar"
136
+ end
137
+
138
+ it "expires the uniqueness key" do
139
+ ShortExpiringJob.enqueue("foo", "bar")
140
+ sleep 2
141
+ ShortExpiringJob.enqueue("foo", "bar")
142
+ worker = Resque::Worker.new(:test)
143
+ worker.work(0)
144
+ WhatHappened.what_happened.should == "ShortExpiringJobfoobarShortExpiringJobfoobar"
145
+ end
146
+
147
+ end
148
+
149
+ end
metadata ADDED
@@ -0,0 +1,117 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: resque-unique-job
3
+ version: !ruby/object:Gem::Version
4
+ hash: 29
5
+ prerelease:
6
+ segments:
7
+ - 0
8
+ - 0
9
+ - 1
10
+ version: 0.0.1
11
+ platform: ruby
12
+ authors:
13
+ - Andy Delcambre
14
+ - Jacob Burkhart
15
+ autorequire:
16
+ bindir: bin
17
+ cert_chain: []
18
+
19
+ date: 2011-03-04 00:00:00 -08:00
20
+ default_executable:
21
+ dependencies:
22
+ - !ruby/object:Gem::Dependency
23
+ name: resque
24
+ prerelease: false
25
+ requirement: &id001 !ruby/object:Gem::Requirement
26
+ none: false
27
+ requirements:
28
+ - - ">="
29
+ - !ruby/object:Gem::Version
30
+ hash: 55
31
+ segments:
32
+ - 1
33
+ - 8
34
+ - 0
35
+ version: 1.8.0
36
+ type: :runtime
37
+ version_requirements: *id001
38
+ - !ruby/object:Gem::Dependency
39
+ name: rspec
40
+ prerelease: false
41
+ requirement: &id002 !ruby/object:Gem::Requirement
42
+ none: false
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ hash: 3
47
+ segments:
48
+ - 0
49
+ version: "0"
50
+ type: :development
51
+ version_requirements: *id002
52
+ - !ruby/object:Gem::Dependency
53
+ name: ruby-debug
54
+ prerelease: false
55
+ requirement: &id003 !ruby/object:Gem::Requirement
56
+ none: false
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ hash: 3
61
+ segments:
62
+ - 0
63
+ version: "0"
64
+ type: :development
65
+ version_requirements: *id003
66
+ description: A Resque plugin for unique jobs
67
+ email: cloud@engineyard.com
68
+ executables: []
69
+
70
+ extensions: []
71
+
72
+ extra_rdoc_files: []
73
+
74
+ files:
75
+ - README.markdown
76
+ - Rakefile
77
+ - LICENSE
78
+ - lib/resque/plugins/resque_ext/destroy.rb
79
+ - lib/resque/plugins/unique_job/version.rb
80
+ - lib/resque/plugins/unique_job.rb
81
+ - spec/unique_job_spec.rb
82
+ has_rdoc: true
83
+ homepage: http://github.com/engineyard/resque-unique-job
84
+ licenses: []
85
+
86
+ post_install_message:
87
+ rdoc_options: []
88
+
89
+ require_paths:
90
+ - lib
91
+ required_ruby_version: !ruby/object:Gem::Requirement
92
+ none: false
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ hash: 3
97
+ segments:
98
+ - 0
99
+ version: "0"
100
+ required_rubygems_version: !ruby/object:Gem::Requirement
101
+ none: false
102
+ requirements:
103
+ - - ">="
104
+ - !ruby/object:Gem::Version
105
+ hash: 3
106
+ segments:
107
+ - 0
108
+ version: "0"
109
+ requirements: []
110
+
111
+ rubyforge_project:
112
+ rubygems_version: 1.5.2
113
+ signing_key:
114
+ specification_version: 3
115
+ summary: A Resque plugin for unique jobs
116
+ test_files: []
117
+