delayed_job 1.8.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,17 @@
1
+ module Delayed
2
+ module MessageSending
3
+ def send_later(method, *args)
4
+ Delayed::Job.enqueue Delayed::PerformableMethod.new(self, method.to_sym, args)
5
+ end
6
+
7
+ module ClassMethods
8
+ def handle_asynchronously(method)
9
+ without_name = "#{method}_without_send_later"
10
+ define_method("#{method}_with_send_later") do |*args|
11
+ send_later(without_name, *args)
12
+ end
13
+ alias_method_chain method, :send_later
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,55 @@
1
+ module Delayed
2
+ class PerformableMethod < Struct.new(:object, :method, :args)
3
+ CLASS_STRING_FORMAT = /^CLASS\:([A-Z][\w\:]+)$/
4
+ AR_STRING_FORMAT = /^AR\:([A-Z][\w\:]+)\:(\d+)$/
5
+
6
+ def initialize(object, method, args)
7
+ raise NoMethodError, "undefined method `#{method}' for #{self.inspect}" unless object.respond_to?(method)
8
+
9
+ self.object = dump(object)
10
+ self.args = args.map { |a| dump(a) }
11
+ self.method = method.to_sym
12
+ end
13
+
14
+ def display_name
15
+ case self.object
16
+ when CLASS_STRING_FORMAT then "#{$1}.#{method}"
17
+ when AR_STRING_FORMAT then "#{$1}##{method}"
18
+ else "Unknown##{method}"
19
+ end
20
+ end
21
+
22
+ def perform
23
+ load(object).send(method, *args.map{|a| load(a)})
24
+ rescue ActiveRecord::RecordNotFound
25
+ # We cannot do anything about objects which were deleted in the meantime
26
+ true
27
+ end
28
+
29
+ private
30
+
31
+ def load(arg)
32
+ case arg
33
+ when CLASS_STRING_FORMAT then $1.constantize
34
+ when AR_STRING_FORMAT then $1.constantize.find($2)
35
+ else arg
36
+ end
37
+ end
38
+
39
+ def dump(arg)
40
+ case arg
41
+ when Class then class_to_string(arg)
42
+ when ActiveRecord::Base then ar_to_string(arg)
43
+ else arg
44
+ end
45
+ end
46
+
47
+ def ar_to_string(obj)
48
+ "AR:#{obj.class}:#{obj.id}"
49
+ end
50
+
51
+ def class_to_string(obj)
52
+ "CLASS:#{obj.name}"
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,15 @@
1
+ # Re-definitions are appended to existing tasks
2
+ task :environment
3
+ task :merb_env
4
+
5
+ namespace :jobs do
6
+ desc "Clear the delayed_job queue."
7
+ task :clear => [:merb_env, :environment] do
8
+ Delayed::Job.delete_all
9
+ end
10
+
11
+ desc "Start a delayed_job worker."
12
+ task :work => [:merb_env, :environment] do
13
+ Delayed::Worker.new(:min_priority => ENV['MIN_PRIORITY'], :max_priority => ENV['MAX_PRIORITY']).start
14
+ end
15
+ end
@@ -0,0 +1,54 @@
1
+ module Delayed
2
+ class Worker
3
+ SLEEP = 5
4
+
5
+ cattr_accessor :logger
6
+ self.logger = if defined?(Merb::Logger)
7
+ Merb.logger
8
+ elsif defined?(RAILS_DEFAULT_LOGGER)
9
+ RAILS_DEFAULT_LOGGER
10
+ end
11
+
12
+ def initialize(options={})
13
+ @quiet = options[:quiet]
14
+ Delayed::Job.min_priority = options[:min_priority] if options.has_key?(:min_priority)
15
+ Delayed::Job.max_priority = options[:max_priority] if options.has_key?(:max_priority)
16
+ end
17
+
18
+ def start
19
+ say "*** Starting job worker #{Delayed::Job.worker_name}"
20
+
21
+ trap('TERM') { say 'Exiting...'; $exit = true }
22
+ trap('INT') { say 'Exiting...'; $exit = true }
23
+
24
+ loop do
25
+ result = nil
26
+
27
+ realtime = Benchmark.realtime do
28
+ result = Delayed::Job.work_off
29
+ end
30
+
31
+ count = result.sum
32
+
33
+ break if $exit
34
+
35
+ if count.zero?
36
+ sleep(SLEEP)
37
+ else
38
+ say "#{count} jobs processed at %.4f j/s, %d failed ..." % [count / realtime, result.last]
39
+ end
40
+
41
+ break if $exit
42
+ end
43
+
44
+ ensure
45
+ Delayed::Job.clear_locks!
46
+ end
47
+
48
+ def say(text)
49
+ puts text unless @quiet
50
+ logger.info text if logger
51
+ end
52
+
53
+ end
54
+ end
@@ -0,0 +1,13 @@
1
+ autoload :ActiveRecord, 'activerecord'
2
+
3
+ require File.dirname(__FILE__) + '/delayed/message_sending'
4
+ require File.dirname(__FILE__) + '/delayed/performable_method'
5
+ require File.dirname(__FILE__) + '/delayed/job'
6
+ require File.dirname(__FILE__) + '/delayed/worker'
7
+
8
+ Object.send(:include, Delayed::MessageSending)
9
+ Module.send(:include, Delayed::MessageSending::ClassMethods)
10
+
11
+ if defined?(Merb::Plugins)
12
+ Merb::Plugins.add_rakefiles File.dirname(__FILE__) / 'delayed' / 'tasks'
13
+ end
@@ -0,0 +1,26 @@
1
+ # Capistrano Recipes for managing delayed_job
2
+ #
3
+ # Add these callbacks to have the delayed_job process restart when the server
4
+ # is restarted:
5
+ #
6
+ # after "deploy:stop", "delayed_job:stop"
7
+ # after "deploy:start", "delayed_job:start"
8
+ # after "deploy:restart", "delayed_job:restart"
9
+
10
+
11
+ namespace :delayed_job do
12
+ desc "Stop the delayed_job process"
13
+ task :stop, :roles => :app do
14
+ run "cd #{current_path}; script/delayed_job -e #{rails_env} stop"
15
+ end
16
+
17
+ desc "Start the delayed_job process"
18
+ task :start, :roles => :app do
19
+ run "cd #{current_path}; script/delayed_job -e #{rails_env} start"
20
+ end
21
+
22
+ desc "Restart the delayed_job process"
23
+ task :restart, :roles => :app do
24
+ run "cd #{current_path}; script/delayed_job -e #{rails_env} restart"
25
+ end
26
+ end
@@ -0,0 +1,42 @@
1
+ $:.unshift(File.dirname(__FILE__) + '/../lib')
2
+ $:.unshift(File.dirname(__FILE__) + '/../../rspec/lib')
3
+
4
+ require 'rubygems'
5
+ require 'active_record'
6
+ gem 'sqlite3-ruby'
7
+
8
+ require File.dirname(__FILE__) + '/../init'
9
+ require 'spec'
10
+
11
+ ActiveRecord::Base.logger = Logger.new('/tmp/dj.log')
12
+ ActiveRecord::Base.establish_connection(:adapter => 'sqlite3', :database => '/tmp/jobs.sqlite')
13
+ ActiveRecord::Migration.verbose = false
14
+
15
+ ActiveRecord::Schema.define do
16
+
17
+ create_table :delayed_jobs, :force => true do |table|
18
+ table.integer :priority, :default => 0
19
+ table.integer :attempts, :default => 0
20
+ table.text :handler
21
+ table.string :last_error
22
+ table.datetime :run_at
23
+ table.datetime :locked_at
24
+ table.string :locked_by
25
+ table.datetime :failed_at
26
+ table.timestamps
27
+ end
28
+
29
+ create_table :stories, :force => true do |table|
30
+ table.string :text
31
+ end
32
+
33
+ end
34
+
35
+
36
+ # Purely useful for test cases...
37
+ class Story < ActiveRecord::Base
38
+ def tell; text; end
39
+ def whatever(n, _); tell*n; end
40
+
41
+ handle_asynchronously :whatever
42
+ end
@@ -0,0 +1,128 @@
1
+ require File.dirname(__FILE__) + '/database'
2
+
3
+ class SimpleJob
4
+ cattr_accessor :runs; self.runs = 0
5
+ def perform; @@runs += 1; end
6
+ end
7
+
8
+ class RandomRubyObject
9
+ def say_hello
10
+ 'hello'
11
+ end
12
+ end
13
+
14
+ class ErrorObject
15
+
16
+ def throw
17
+ raise ActiveRecord::RecordNotFound, '...'
18
+ false
19
+ end
20
+
21
+ end
22
+
23
+ class StoryReader
24
+
25
+ def read(story)
26
+ "Epilog: #{story.tell}"
27
+ end
28
+
29
+ end
30
+
31
+ class StoryReader
32
+
33
+ def read(story)
34
+ "Epilog: #{story.tell}"
35
+ end
36
+
37
+ end
38
+
39
+ describe 'random ruby objects' do
40
+ before { Delayed::Job.delete_all }
41
+
42
+ it "should respond_to :send_later method" do
43
+
44
+ RandomRubyObject.new.respond_to?(:send_later)
45
+
46
+ end
47
+
48
+ it "should raise a ArgumentError if send_later is called but the target method doesn't exist" do
49
+ lambda { RandomRubyObject.new.send_later(:method_that_deos_not_exist) }.should raise_error(NoMethodError)
50
+ end
51
+
52
+ it "should add a new entry to the job table when send_later is called on it" do
53
+ Delayed::Job.count.should == 0
54
+
55
+ RandomRubyObject.new.send_later(:to_s)
56
+
57
+ Delayed::Job.count.should == 1
58
+ end
59
+
60
+ it "should add a new entry to the job table when send_later is called on the class" do
61
+ Delayed::Job.count.should == 0
62
+
63
+ RandomRubyObject.send_later(:to_s)
64
+
65
+ Delayed::Job.count.should == 1
66
+ end
67
+
68
+ it "should run get the original method executed when the job is performed" do
69
+
70
+ RandomRubyObject.new.send_later(:say_hello)
71
+
72
+ Delayed::Job.count.should == 1
73
+ end
74
+
75
+ it "should ignore ActiveRecord::RecordNotFound errors because they are permanent" do
76
+
77
+ ErrorObject.new.send_later(:throw)
78
+
79
+ Delayed::Job.count.should == 1
80
+
81
+ Delayed::Job.reserve_and_run_one_job
82
+
83
+ Delayed::Job.count.should == 0
84
+
85
+ end
86
+
87
+ it "should store the object as string if its an active record" do
88
+ story = Story.create :text => 'Once upon...'
89
+ story.send_later(:tell)
90
+
91
+ job = Delayed::Job.find(:first)
92
+ job.payload_object.class.should == Delayed::PerformableMethod
93
+ job.payload_object.object.should == "AR:Story:#{story.id}"
94
+ job.payload_object.method.should == :tell
95
+ job.payload_object.args.should == []
96
+ job.payload_object.perform.should == 'Once upon...'
97
+ end
98
+
99
+ it "should store arguments as string if they an active record" do
100
+
101
+ story = Story.create :text => 'Once upon...'
102
+
103
+ reader = StoryReader.new
104
+ reader.send_later(:read, story)
105
+
106
+ job = Delayed::Job.find(:first)
107
+ job.payload_object.class.should == Delayed::PerformableMethod
108
+ job.payload_object.method.should == :read
109
+ job.payload_object.args.should == ["AR:Story:#{story.id}"]
110
+ job.payload_object.perform.should == 'Epilog: Once upon...'
111
+ end
112
+
113
+ it "should call send later on methods which are wrapped with handle_asynchronously" do
114
+ story = Story.create :text => 'Once upon...'
115
+
116
+ Delayed::Job.count.should == 0
117
+
118
+ story.whatever(1, 5)
119
+
120
+ Delayed::Job.count.should == 1
121
+ job = Delayed::Job.find(:first)
122
+ job.payload_object.class.should == Delayed::PerformableMethod
123
+ job.payload_object.method.should == :whatever_without_send_later
124
+ job.payload_object.args.should == [1, 5]
125
+ job.payload_object.perform.should == 'Once upon...'
126
+ end
127
+
128
+ end
@@ -0,0 +1,389 @@
1
+ require File.dirname(__FILE__) + '/database'
2
+
3
+ class SimpleJob
4
+ cattr_accessor :runs; self.runs = 0
5
+ def perform; @@runs += 1; end
6
+ end
7
+
8
+ class ErrorJob
9
+ cattr_accessor :runs; self.runs = 0
10
+ def perform; raise 'did not work'; end
11
+ end
12
+
13
+ class LongRunningJob
14
+ def perform; sleep 250; end
15
+ end
16
+
17
+ module M
18
+ class ModuleJob
19
+ cattr_accessor :runs; self.runs = 0
20
+ def perform; @@runs += 1; end
21
+ end
22
+
23
+ end
24
+
25
+ describe Delayed::Job do
26
+ before do
27
+ Delayed::Job.max_priority = nil
28
+ Delayed::Job.min_priority = nil
29
+
30
+ Delayed::Job.delete_all
31
+ end
32
+
33
+ before(:each) do
34
+ SimpleJob.runs = 0
35
+ end
36
+
37
+ it "should set run_at automatically if not set" do
38
+ Delayed::Job.create(:payload_object => ErrorJob.new ).run_at.should_not == nil
39
+ end
40
+
41
+ it "should not set run_at automatically if already set" do
42
+ later = 5.minutes.from_now
43
+ Delayed::Job.create(:payload_object => ErrorJob.new, :run_at => later).run_at.should == later
44
+ end
45
+
46
+ it "should raise ArgumentError when handler doesn't respond_to :perform" do
47
+ lambda { Delayed::Job.enqueue(Object.new) }.should raise_error(ArgumentError)
48
+ end
49
+
50
+ it "should increase count after enqueuing items" do
51
+ Delayed::Job.enqueue SimpleJob.new
52
+ Delayed::Job.count.should == 1
53
+ end
54
+
55
+ it "should be able to set priority when enqueuing items" do
56
+ Delayed::Job.enqueue SimpleJob.new, 5
57
+ Delayed::Job.first.priority.should == 5
58
+ end
59
+
60
+ it "should be able to set run_at when enqueuing items" do
61
+ later = 5.minutes.from_now
62
+ Delayed::Job.enqueue SimpleJob.new, 5, later
63
+
64
+ # use be close rather than equal to because millisecond values cn be lost in DB round trip
65
+ Delayed::Job.first.run_at.should be_close(later, 1)
66
+ end
67
+
68
+ it "should call perform on jobs when running work_off" do
69
+ SimpleJob.runs.should == 0
70
+
71
+ Delayed::Job.enqueue SimpleJob.new
72
+ Delayed::Job.work_off
73
+
74
+ SimpleJob.runs.should == 1
75
+ end
76
+
77
+
78
+ it "should work with eval jobs" do
79
+ $eval_job_ran = false
80
+
81
+ Delayed::Job.enqueue do <<-JOB
82
+ $eval_job_ran = true
83
+ JOB
84
+ end
85
+
86
+ Delayed::Job.work_off
87
+
88
+ $eval_job_ran.should == true
89
+ end
90
+
91
+ it "should work with jobs in modules" do
92
+ M::ModuleJob.runs.should == 0
93
+
94
+ Delayed::Job.enqueue M::ModuleJob.new
95
+ Delayed::Job.work_off
96
+
97
+ M::ModuleJob.runs.should == 1
98
+ end
99
+
100
+ it "should re-schedule by about 1 second at first and increment this more and more minutes when it fails to execute properly" do
101
+ Delayed::Job.enqueue ErrorJob.new
102
+ Delayed::Job.work_off(1)
103
+
104
+ job = Delayed::Job.find(:first)
105
+
106
+ job.last_error.should =~ /did not work/
107
+ job.last_error.should =~ /job_spec.rb:10:in `perform'/
108
+ job.attempts.should == 1
109
+
110
+ job.run_at.should > Delayed::Job.db_time_now - 10.minutes
111
+ job.run_at.should < Delayed::Job.db_time_now + 10.minutes
112
+ end
113
+
114
+ it "should raise an DeserializationError when the job class is totally unknown" do
115
+
116
+ job = Delayed::Job.new
117
+ job['handler'] = "--- !ruby/object:JobThatDoesNotExist {}"
118
+
119
+ lambda { job.payload_object.perform }.should raise_error(Delayed::DeserializationError)
120
+ end
121
+
122
+ it "should try to load the class when it is unknown at the time of the deserialization" do
123
+ job = Delayed::Job.new
124
+ job['handler'] = "--- !ruby/object:JobThatDoesNotExist {}"
125
+
126
+ job.should_receive(:attempt_to_load).with('JobThatDoesNotExist').and_return(true)
127
+
128
+ lambda { job.payload_object.perform }.should raise_error(Delayed::DeserializationError)
129
+ end
130
+
131
+ it "should try include the namespace when loading unknown objects" do
132
+ job = Delayed::Job.new
133
+ job['handler'] = "--- !ruby/object:Delayed::JobThatDoesNotExist {}"
134
+ job.should_receive(:attempt_to_load).with('Delayed::JobThatDoesNotExist').and_return(true)
135
+ lambda { job.payload_object.perform }.should raise_error(Delayed::DeserializationError)
136
+ end
137
+
138
+ it "should also try to load structs when they are unknown (raises TypeError)" do
139
+ job = Delayed::Job.new
140
+ job['handler'] = "--- !ruby/struct:JobThatDoesNotExist {}"
141
+
142
+ job.should_receive(:attempt_to_load).with('JobThatDoesNotExist').and_return(true)
143
+
144
+ lambda { job.payload_object.perform }.should raise_error(Delayed::DeserializationError)
145
+ end
146
+
147
+ it "should try include the namespace when loading unknown structs" do
148
+ job = Delayed::Job.new
149
+ job['handler'] = "--- !ruby/struct:Delayed::JobThatDoesNotExist {}"
150
+
151
+ job.should_receive(:attempt_to_load).with('Delayed::JobThatDoesNotExist').and_return(true)
152
+ lambda { job.payload_object.perform }.should raise_error(Delayed::DeserializationError)
153
+ end
154
+
155
+ it "should be failed if it failed more than MAX_ATTEMPTS times and we don't want to destroy jobs" do
156
+ default = Delayed::Job.destroy_failed_jobs
157
+ Delayed::Job.destroy_failed_jobs = false
158
+
159
+ @job = Delayed::Job.create :payload_object => SimpleJob.new, :attempts => 50
160
+ @job.reload.failed_at.should == nil
161
+ @job.reschedule 'FAIL'
162
+ @job.reload.failed_at.should_not == nil
163
+
164
+ Delayed::Job.destroy_failed_jobs = default
165
+ end
166
+
167
+ it "should be destroyed if it failed more than MAX_ATTEMPTS times and we want to destroy jobs" do
168
+ default = Delayed::Job.destroy_failed_jobs
169
+ Delayed::Job.destroy_failed_jobs = true
170
+
171
+ @job = Delayed::Job.create :payload_object => SimpleJob.new, :attempts => 50
172
+ @job.should_receive(:destroy)
173
+ @job.reschedule 'FAIL'
174
+
175
+ Delayed::Job.destroy_failed_jobs = default
176
+ end
177
+
178
+ it "should fail after MAX_RUN_TIME" do
179
+ @job = Delayed::Job.create :payload_object => LongRunningJob.new
180
+ Delayed::Job.reserve_and_run_one_job(1.second)
181
+ @job.reload.last_error.should =~ /expired/
182
+ @job.attempts.should == 1
183
+ end
184
+
185
+ it "should never find failed jobs" do
186
+ @job = Delayed::Job.create :payload_object => SimpleJob.new, :attempts => 50, :failed_at => Time.now
187
+ Delayed::Job.find_available(1).length.should == 0
188
+ end
189
+
190
+ context "when another worker is already performing an task, it" do
191
+
192
+ before :each do
193
+ Delayed::Job.worker_name = 'worker1'
194
+ @job = Delayed::Job.create :payload_object => SimpleJob.new, :locked_by => 'worker1', :locked_at => Delayed::Job.db_time_now - 5.minutes
195
+ end
196
+
197
+ it "should not allow a second worker to get exclusive access" do
198
+ @job.lock_exclusively!(4.hours, 'worker2').should == false
199
+ end
200
+
201
+ it "should allow a second worker to get exclusive access if the timeout has passed" do
202
+ @job.lock_exclusively!(1.minute, 'worker2').should == true
203
+ end
204
+
205
+ it "should be able to get access to the task if it was started more then max_age ago" do
206
+ @job.locked_at = 5.hours.ago
207
+ @job.save
208
+
209
+ @job.lock_exclusively! 4.hours, 'worker2'
210
+ @job.reload
211
+ @job.locked_by.should == 'worker2'
212
+ @job.locked_at.should > 1.minute.ago
213
+ end
214
+
215
+ it "should not be found by another worker" do
216
+ Delayed::Job.worker_name = 'worker2'
217
+
218
+ Delayed::Job.find_available(1, 6.minutes).length.should == 0
219
+ end
220
+
221
+ it "should be found by another worker if the time has expired" do
222
+ Delayed::Job.worker_name = 'worker2'
223
+
224
+ Delayed::Job.find_available(1, 4.minutes).length.should == 1
225
+ end
226
+
227
+ it "should be able to get exclusive access again when the worker name is the same" do
228
+ @job.lock_exclusively! 5.minutes, 'worker1'
229
+ @job.lock_exclusively! 5.minutes, 'worker1'
230
+ @job.lock_exclusively! 5.minutes, 'worker1'
231
+ end
232
+ end
233
+
234
+ context "when another worker has worked on a task since the job was found to be available, it" do
235
+
236
+ before :each do
237
+ Delayed::Job.worker_name = 'worker1'
238
+ @job = Delayed::Job.create :payload_object => SimpleJob.new
239
+ @job_copy_for_worker_2 = Delayed::Job.find(@job.id)
240
+ end
241
+
242
+ it "should not allow a second worker to get exclusive access if already successfully processed by worker1" do
243
+ @job.delete
244
+ @job_copy_for_worker_2.lock_exclusively!(4.hours, 'worker2').should == false
245
+ end
246
+
247
+ it "should not allow a second worker to get exclusive access if failed to be processed by worker1 and run_at time is now in future (due to backing off behaviour)" do
248
+ @job.update_attributes(:attempts => 1, :run_at => Time.now + 1.day)
249
+ @job_copy_for_worker_2.lock_exclusively!(4.hours, 'worker2').should == false
250
+ end
251
+ end
252
+
253
+ context "#name" do
254
+ it "should be the class name of the job that was enqueued" do
255
+ Delayed::Job.create(:payload_object => ErrorJob.new ).name.should == 'ErrorJob'
256
+ end
257
+
258
+ it "should be the method that will be called if its a performable method object" do
259
+ Delayed::Job.send_later(:clear_locks!)
260
+ Delayed::Job.last.name.should == 'Delayed::Job.clear_locks!'
261
+
262
+ end
263
+ it "should be the instance method that will be called if its a performable method object" do
264
+ story = Story.create :text => "..."
265
+
266
+ story.send_later(:save)
267
+
268
+ Delayed::Job.last.name.should == 'Story#save'
269
+ end
270
+ end
271
+
272
+ context "worker prioritization" do
273
+
274
+ before(:each) do
275
+ Delayed::Job.max_priority = nil
276
+ Delayed::Job.min_priority = nil
277
+ end
278
+
279
+ it "should only work_off jobs that are >= min_priority" do
280
+ Delayed::Job.min_priority = -5
281
+ Delayed::Job.max_priority = 5
282
+ SimpleJob.runs.should == 0
283
+
284
+ Delayed::Job.enqueue SimpleJob.new, -10
285
+ Delayed::Job.enqueue SimpleJob.new, 0
286
+ Delayed::Job.work_off
287
+
288
+ SimpleJob.runs.should == 1
289
+ end
290
+
291
+ it "should only work_off jobs that are <= max_priority" do
292
+ Delayed::Job.min_priority = -5
293
+ Delayed::Job.max_priority = 5
294
+ SimpleJob.runs.should == 0
295
+
296
+ Delayed::Job.enqueue SimpleJob.new, 10
297
+ Delayed::Job.enqueue SimpleJob.new, 0
298
+
299
+ Delayed::Job.work_off
300
+
301
+ SimpleJob.runs.should == 1
302
+ end
303
+
304
+ it "should fetch jobs ordered by priority" do
305
+ number_of_jobs = 10
306
+ number_of_jobs.times { Delayed::Job.enqueue SimpleJob.new, rand(10) }
307
+ jobs = Delayed::Job.find_available(10)
308
+ ordered = true
309
+ jobs[1..-1].each_index{ |i|
310
+ if (jobs[i].priority < jobs[i+1].priority)
311
+ ordered = false
312
+ break
313
+ end
314
+ }
315
+ ordered.should == true
316
+ end
317
+
318
+ end
319
+
320
+ context "when pulling jobs off the queue for processing, it" do
321
+ before(:each) do
322
+ @job = Delayed::Job.create(
323
+ :payload_object => SimpleJob.new,
324
+ :locked_by => 'worker1',
325
+ :locked_at => Delayed::Job.db_time_now - 5.minutes)
326
+ end
327
+
328
+ it "should leave the queue in a consistent state and not run the job if locking fails" do
329
+ SimpleJob.runs.should == 0
330
+ @job.stub!(:lock_exclusively!).with(any_args).once.and_return(false)
331
+ Delayed::Job.should_receive(:find_available).once.and_return([@job])
332
+ Delayed::Job.work_off(1)
333
+ SimpleJob.runs.should == 0
334
+ end
335
+
336
+ end
337
+
338
+ context "while running alongside other workers that locked jobs, it" do
339
+ before(:each) do
340
+ Delayed::Job.worker_name = 'worker1'
341
+ Delayed::Job.create(:payload_object => SimpleJob.new, :locked_by => 'worker1', :locked_at => (Delayed::Job.db_time_now - 1.minutes))
342
+ Delayed::Job.create(:payload_object => SimpleJob.new, :locked_by => 'worker2', :locked_at => (Delayed::Job.db_time_now - 1.minutes))
343
+ Delayed::Job.create(:payload_object => SimpleJob.new)
344
+ Delayed::Job.create(:payload_object => SimpleJob.new, :locked_by => 'worker1', :locked_at => (Delayed::Job.db_time_now - 1.minutes))
345
+ end
346
+
347
+ it "should ingore locked jobs from other workers" do
348
+ Delayed::Job.worker_name = 'worker3'
349
+ SimpleJob.runs.should == 0
350
+ Delayed::Job.work_off
351
+ SimpleJob.runs.should == 1 # runs the one open job
352
+ end
353
+
354
+ it "should find our own jobs regardless of locks" do
355
+ Delayed::Job.worker_name = 'worker1'
356
+ SimpleJob.runs.should == 0
357
+ Delayed::Job.work_off
358
+ SimpleJob.runs.should == 3 # runs open job plus worker1 jobs that were already locked
359
+ end
360
+ end
361
+
362
+ context "while running with locked and expired jobs, it" do
363
+ before(:each) do
364
+ Delayed::Job.worker_name = 'worker1'
365
+ exp_time = Delayed::Job.db_time_now - (1.minutes + Delayed::Job::MAX_RUN_TIME)
366
+ Delayed::Job.create(:payload_object => SimpleJob.new, :locked_by => 'worker1', :locked_at => exp_time)
367
+ Delayed::Job.create(:payload_object => SimpleJob.new, :locked_by => 'worker2', :locked_at => (Delayed::Job.db_time_now - 1.minutes))
368
+ Delayed::Job.create(:payload_object => SimpleJob.new)
369
+ Delayed::Job.create(:payload_object => SimpleJob.new, :locked_by => 'worker1', :locked_at => (Delayed::Job.db_time_now - 1.minutes))
370
+ end
371
+
372
+ it "should only find unlocked and expired jobs" do
373
+ Delayed::Job.worker_name = 'worker3'
374
+ SimpleJob.runs.should == 0
375
+ Delayed::Job.work_off
376
+ SimpleJob.runs.should == 2 # runs the one open job and one expired job
377
+ end
378
+
379
+ it "should ignore locks when finding our own jobs" do
380
+ Delayed::Job.worker_name = 'worker1'
381
+ SimpleJob.runs.should == 0
382
+ Delayed::Job.work_off
383
+ SimpleJob.runs.should == 3 # runs open job plus worker1 jobs
384
+ # This is useful in the case of a crash/restart on worker1, but make sure multiple workers on the same host have unique names!
385
+ end
386
+
387
+ end
388
+
389
+ end