resque-unique-job 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.
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
+