xspond-delayed_job 1.8.5

Sign up to get free protection for your applications and to get access to all the features.
data/spec/job_spec.rb ADDED
@@ -0,0 +1,315 @@
1
+ require File.dirname(__FILE__) + '/database'
2
+ require File.dirname(__FILE__) + '/sample_jobs'
3
+
4
+ describe Delayed::Job do
5
+ before do
6
+ Delayed::Job.max_priority = nil
7
+ Delayed::Job.min_priority = nil
8
+
9
+ Delayed::Job.delete_all
10
+ end
11
+
12
+ before(:each) do
13
+ SimpleJob.runs = 0
14
+ end
15
+
16
+ it "should set run_at automatically if not set" do
17
+ Delayed::Job.create(:payload_object => ErrorJob.new ).run_at.should_not == nil
18
+ end
19
+
20
+ it "should not set run_at automatically if already set" do
21
+ later = 5.minutes.from_now
22
+ Delayed::Job.create(:payload_object => ErrorJob.new, :run_at => later).run_at.should == later
23
+ end
24
+
25
+ it "should raise ArgumentError when handler doesn't respond_to :perform" do
26
+ lambda { Delayed::Job.enqueue(Object.new) }.should raise_error(ArgumentError)
27
+ end
28
+
29
+ it "should increase count after enqueuing items" do
30
+ Delayed::Job.enqueue SimpleJob.new
31
+ Delayed::Job.count.should == 1
32
+ end
33
+
34
+ it "should be able to set priority when enqueuing items" do
35
+ Delayed::Job.enqueue SimpleJob.new, 5
36
+ Delayed::Job.first.priority.should == 5
37
+ end
38
+
39
+ it "should be able to set run_at when enqueuing items" do
40
+ later = (Delayed::Job.db_time_now+5.minutes)
41
+ Delayed::Job.enqueue SimpleJob.new, 5, later
42
+
43
+ # use be close rather than equal to because millisecond values cn be lost in DB round trip
44
+ Delayed::Job.first.run_at.should be_close(later, 1)
45
+ end
46
+
47
+ it "should call perform on jobs when running run_with_lock" do
48
+ SimpleJob.runs.should == 0
49
+
50
+ job = Delayed::Job.enqueue SimpleJob.new
51
+ job.run_with_lock(Delayed::Job.max_run_time, 'worker')
52
+
53
+ SimpleJob.runs.should == 1
54
+ end
55
+
56
+
57
+ it "should work with eval jobs" do
58
+ $eval_job_ran = false
59
+
60
+ job = Delayed::Job.enqueue do <<-JOB
61
+ $eval_job_ran = true
62
+ JOB
63
+ end
64
+
65
+ job.run_with_lock(Delayed::Job.max_run_time, 'worker')
66
+
67
+ $eval_job_ran.should == true
68
+ end
69
+
70
+ it "should work with jobs in modules" do
71
+ M::ModuleJob.runs.should == 0
72
+
73
+ job = Delayed::Job.enqueue M::ModuleJob.new
74
+ job.run_with_lock(Delayed::Job.max_run_time, 'worker')
75
+
76
+ M::ModuleJob.runs.should == 1
77
+ end
78
+
79
+ it "should re-schedule by about 1 second at first and increment this more and more minutes when it fails to execute properly" do
80
+ job = Delayed::Job.enqueue ErrorJob.new
81
+ job.run_with_lock(Delayed::Job.max_run_time, 'worker')
82
+
83
+ job = Delayed::Job.find(:first)
84
+
85
+ job.last_error.should =~ /did not work/
86
+ job.last_error.should =~ /sample_jobs.rb:8:in `perform'/
87
+ job.attempts.should == 1
88
+
89
+ job.run_at.should > Delayed::Job.db_time_now - 10.minutes
90
+ job.run_at.should < Delayed::Job.db_time_now + 10.minutes
91
+ end
92
+
93
+ it "should record last_error when destroy_failed_jobs = false, max_attempts = 1" do
94
+ Delayed::Job.destroy_failed_jobs = false
95
+ Delayed::Job::max_attempts = 1
96
+ job = Delayed::Job.enqueue ErrorJob.new
97
+ job.run(1)
98
+ job.reload
99
+ job.last_error.should =~ /did not work/
100
+ job.last_error.should =~ /job_spec.rb/
101
+ job.attempts.should == 1
102
+
103
+ job.failed_at.should_not == nil
104
+ end
105
+
106
+ it "should raise an DeserializationError when the job class is totally unknown" do
107
+
108
+ job = Delayed::Job.new
109
+ job['handler'] = "--- !ruby/object:JobThatDoesNotExist {}"
110
+
111
+ lambda { job.payload_object.perform }.should raise_error(Delayed::DeserializationError)
112
+ end
113
+
114
+ it "should try to load the class when it is unknown at the time of the deserialization" do
115
+ job = Delayed::Job.new
116
+ job['handler'] = "--- !ruby/object:JobThatDoesNotExist {}"
117
+
118
+ job.should_receive(:attempt_to_load).with('JobThatDoesNotExist').and_return(true)
119
+
120
+ lambda { job.payload_object.perform }.should raise_error(Delayed::DeserializationError)
121
+ end
122
+
123
+ it "should try include the namespace when loading unknown objects" do
124
+ job = Delayed::Job.new
125
+ job['handler'] = "--- !ruby/object:Delayed::JobThatDoesNotExist {}"
126
+ job.should_receive(:attempt_to_load).with('Delayed::JobThatDoesNotExist').and_return(true)
127
+ lambda { job.payload_object.perform }.should raise_error(Delayed::DeserializationError)
128
+ end
129
+
130
+ it "should also try to load structs when they are unknown (raises TypeError)" do
131
+ job = Delayed::Job.new
132
+ job['handler'] = "--- !ruby/struct:JobThatDoesNotExist {}"
133
+
134
+ job.should_receive(:attempt_to_load).with('JobThatDoesNotExist').and_return(true)
135
+
136
+ lambda { job.payload_object.perform }.should raise_error(Delayed::DeserializationError)
137
+ end
138
+
139
+ it "should try include the namespace when loading unknown structs" do
140
+ job = Delayed::Job.new
141
+ job['handler'] = "--- !ruby/struct:Delayed::JobThatDoesNotExist {}"
142
+
143
+ job.should_receive(:attempt_to_load).with('Delayed::JobThatDoesNotExist').and_return(true)
144
+ lambda { job.payload_object.perform }.should raise_error(Delayed::DeserializationError)
145
+ end
146
+
147
+ context "reschedule" do
148
+ before do
149
+ @job = Delayed::Job.create :payload_object => SimpleJob.new
150
+ end
151
+
152
+ context "and we want to destroy jobs" do
153
+ before do
154
+ Delayed::Job.destroy_failed_jobs = true
155
+ end
156
+
157
+ it "should be destroyed if it failed more than Job::max_attempts times" do
158
+ @job.should_receive(:destroy)
159
+ Delayed::Job::max_attempts.times { @job.reschedule 'FAIL' }
160
+ end
161
+
162
+ it "should not be destroyed if failed fewer than Job::max_attempts times" do
163
+ @job.should_not_receive(:destroy)
164
+ (Delayed::Job::max_attempts - 1).times { @job.reschedule 'FAIL' }
165
+ end
166
+ end
167
+
168
+ context "and we don't want to destroy jobs" do
169
+ before do
170
+ Delayed::Job.destroy_failed_jobs = false
171
+ end
172
+
173
+ it "should be failed if it failed more than Job::max_attempts times" do
174
+ @job.reload.failed_at.should == nil
175
+ Delayed::Job::max_attempts.times { @job.reschedule 'FAIL' }
176
+ @job.reload.failed_at.should_not == nil
177
+ end
178
+
179
+ it "should not be failed if it failed fewer than Job::max_attempts times" do
180
+ (Delayed::Job::max_attempts - 1).times { @job.reschedule 'FAIL' }
181
+ @job.reload.failed_at.should == nil
182
+ end
183
+
184
+ end
185
+ end
186
+
187
+ it "should fail after Job::max_run_time" do
188
+ @job = Delayed::Job.create :payload_object => LongRunningJob.new
189
+ @job.run_with_lock(1.second, 'worker')
190
+ @job.reload.last_error.should =~ /expired/
191
+ @job.attempts.should == 1
192
+ end
193
+
194
+ it "should never find failed jobs" do
195
+ @job = Delayed::Job.create :payload_object => SimpleJob.new, :attempts => 50, :failed_at => Delayed::Job.db_time_now
196
+ Delayed::Job.find_available('worker', 1).length.should == 0
197
+ end
198
+
199
+ context "when another worker is already performing an task, it" do
200
+
201
+ before :each do
202
+ @job = Delayed::Job.create :payload_object => SimpleJob.new, :locked_by => 'worker1', :locked_at => Delayed::Job.db_time_now - 5.minutes
203
+ end
204
+
205
+ it "should not allow a second worker to get exclusive access" do
206
+ @job.lock_exclusively!(4.hours, 'worker2').should == false
207
+ end
208
+
209
+ it "should allow a second worker to get exclusive access if the timeout has passed" do
210
+ @job.lock_exclusively!(1.minute, 'worker2').should == true
211
+ end
212
+
213
+ it "should be able to get access to the task if it was started more then max_age ago" do
214
+ @job.locked_at = 5.hours.ago
215
+ @job.save
216
+
217
+ @job.lock_exclusively! 4.hours, 'worker2'
218
+ @job.reload
219
+ @job.locked_by.should == 'worker2'
220
+ @job.locked_at.should > 1.minute.ago
221
+ end
222
+
223
+ it "should not be found by another worker" do
224
+ Delayed::Job.find_available('worker2', 1, 6.minutes).length.should == 0
225
+ end
226
+
227
+ it "should be found by another worker if the time has expired" do
228
+ Delayed::Job.find_available('worker2', 1, 4.minutes).length.should == 1
229
+ end
230
+
231
+ it "should be able to get exclusive access again when the worker name is the same" do
232
+ @job.lock_exclusively!(5.minutes, 'worker1').should be_true
233
+ @job.lock_exclusively!(5.minutes, 'worker1').should be_true
234
+ @job.lock_exclusively!(5.minutes, 'worker1').should be_true
235
+ end
236
+ end
237
+
238
+ context "when another worker has worked on a task since the job was found to be available, it" do
239
+
240
+ before :each do
241
+ @job = Delayed::Job.create :payload_object => SimpleJob.new
242
+ @job_copy_for_worker_2 = Delayed::Job.find(@job.id)
243
+ end
244
+
245
+ it "should not allow a second worker to get exclusive access if already successfully processed by worker1" do
246
+ @job.delete
247
+ @job_copy_for_worker_2.lock_exclusively!(4.hours, 'worker2').should == false
248
+ end
249
+
250
+ 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
251
+ @job.update_attributes(:attempts => 1, :run_at => 1.day.from_now)
252
+ @job_copy_for_worker_2.lock_exclusively!(4.hours, 'worker2').should == false
253
+ end
254
+ end
255
+
256
+ context "#name" do
257
+ it "should be the class name of the job that was enqueued" do
258
+ Delayed::Job.create(:payload_object => ErrorJob.new ).name.should == 'ErrorJob'
259
+ end
260
+
261
+ it "should be the method that will be called if its a performable method object" do
262
+ Delayed::Job.send_later(:clear_locks!)
263
+ Delayed::Job.last.name.should == 'Delayed::Job.clear_locks!'
264
+
265
+ end
266
+ it "should be the instance method that will be called if its a performable method object" do
267
+ story = Story.create :text => "..."
268
+
269
+ story.send_later(:save)
270
+
271
+ Delayed::Job.last.name.should == 'Story#save'
272
+ end
273
+ end
274
+
275
+ context "worker prioritization" do
276
+
277
+ before(:each) do
278
+ Delayed::Job.max_priority = nil
279
+ Delayed::Job.min_priority = nil
280
+ end
281
+
282
+ it "should fetch jobs ordered by priority" do
283
+ number_of_jobs = 10
284
+ number_of_jobs.times { Delayed::Job.enqueue SimpleJob.new, rand(10) }
285
+ jobs = Delayed::Job.find_available('worker', 10)
286
+ ordered = true
287
+ jobs[1..-1].each_index{ |i|
288
+ if (jobs[i].priority < jobs[i+1].priority)
289
+ ordered = false
290
+ break
291
+ end
292
+ }
293
+ ordered.should == true
294
+ end
295
+
296
+ end
297
+
298
+ context "when pulling jobs off the queue for processing, it" do
299
+ before(:each) do
300
+ @job = Delayed::Job.create(
301
+ :payload_object => SimpleJob.new,
302
+ :locked_by => 'worker1',
303
+ :locked_at => Delayed::Job.db_time_now - 5.minutes)
304
+ end
305
+
306
+ it "should leave the queue in a consistent state and not run the job if locking fails" do
307
+ SimpleJob.runs.should == 0
308
+ @job.stub!(:lock_exclusively!).with(any_args).once.and_return(false)
309
+ @job.run_with_lock(Delayed::Job.max_run_time, 'worker')
310
+ SimpleJob.runs.should == 0
311
+ end
312
+
313
+ end
314
+
315
+ end
@@ -0,0 +1,21 @@
1
+ class SimpleJob
2
+ cattr_accessor :runs; self.runs = 0
3
+ def perform; @@runs += 1; end
4
+ end
5
+
6
+ class ErrorJob
7
+ cattr_accessor :runs; self.runs = 0
8
+ def perform; raise 'did not work'; end
9
+ end
10
+
11
+ class LongRunningJob
12
+ def perform; sleep 250; end
13
+ end
14
+
15
+ module M
16
+ class ModuleJob
17
+ cattr_accessor :runs; self.runs = 0
18
+ def perform; @@runs += 1; end
19
+ end
20
+
21
+ end
@@ -0,0 +1,17 @@
1
+ require File.dirname(__FILE__) + '/database'
2
+
3
+ describe "A story" do
4
+
5
+ before(:all) do
6
+ @story = Story.create :text => "Once upon a time..."
7
+ end
8
+
9
+ it "should be shared" do
10
+ @story.tell.should == 'Once upon a time...'
11
+ end
12
+
13
+ it "should not return its result if it storytelling is delayed" do
14
+ @story.send_later(:tell).should_not == 'Once upon a time...'
15
+ end
16
+
17
+ end
@@ -0,0 +1,105 @@
1
+ require File.dirname(__FILE__) + '/database'
2
+ require File.dirname(__FILE__) + '/sample_jobs'
3
+
4
+ describe Delayed::Worker do
5
+ def job_create(opts = {})
6
+ Delayed::Job.create(opts.merge(:payload_object => SimpleJob.new))
7
+ end
8
+
9
+ before do
10
+ Delayed::Worker.class_eval('public :work_off')
11
+ end
12
+
13
+ before(:each) do
14
+ @worker = Delayed::Worker.new(:max_priority => nil, :min_priority => nil)
15
+
16
+ Delayed::Job.delete_all
17
+
18
+ SimpleJob.runs = 0
19
+ end
20
+
21
+ context "worker prioritization" do
22
+ before(:each) do
23
+ @worker = Delayed::Worker.new(:max_priority => 5, :min_priority => -5)
24
+ end
25
+
26
+ it "should only work_off jobs that are >= min_priority" do
27
+ pending('Needs to work with forked workers')
28
+ SimpleJob.runs.should == 0
29
+
30
+ job_create(:priority => -10)
31
+ job_create(:priority => 0)
32
+ @worker.work_off
33
+
34
+ SimpleJob.runs.should == 1
35
+ end
36
+
37
+ it "should only work_off jobs that are <= max_priority" do
38
+ pending('Needs to work with forked workers')
39
+ SimpleJob.runs.should == 0
40
+
41
+ job_create(:priority => 10)
42
+ job_create(:priority => 0)
43
+
44
+ @worker.work_off
45
+
46
+ SimpleJob.runs.should == 1
47
+ end
48
+ end
49
+
50
+ context "while running alongside other workers that locked jobs, it" do
51
+ before(:each) do
52
+ @worker.name = 'worker1'
53
+ job_create(:locked_by => 'worker1', :locked_at => (Delayed::Job.db_time_now - 1.minutes))
54
+ job_create(:locked_by => 'worker2', :locked_at => (Delayed::Job.db_time_now - 1.minutes))
55
+ job_create
56
+ job_create(:locked_by => 'worker1', :locked_at => (Delayed::Job.db_time_now - 1.minutes))
57
+ end
58
+
59
+ it "should ingore locked jobs from other workers" do
60
+ pending('Needs to work with forked workers')
61
+ @worker.name = 'worker3'
62
+ SimpleJob.runs.should == 0
63
+ @worker.work_off
64
+ SimpleJob.runs.should == 1 # runs the one open job
65
+ end
66
+
67
+ it "should find our own jobs regardless of locks" do
68
+ pending('Needs to work with forked workers')
69
+ @worker.name = 'worker1'
70
+ SimpleJob.runs.should == 0
71
+ @worker.work_off
72
+ SimpleJob.runs.should == 3 # runs open job plus worker1 jobs that were already locked
73
+ end
74
+ end
75
+
76
+ context "while running with locked and expired jobs, it" do
77
+ before(:each) do
78
+ @worker.name = 'worker1'
79
+ exp_time = Delayed::Job.db_time_now - (1.minutes + Delayed::Job::max_run_time)
80
+ job_create(:locked_by => 'worker1', :locked_at => exp_time)
81
+ job_create(:locked_by => 'worker2', :locked_at => (Delayed::Job.db_time_now - 1.minutes))
82
+ job_create
83
+ job_create(:locked_by => 'worker1', :locked_at => (Delayed::Job.db_time_now - 1.minutes))
84
+ end
85
+
86
+ it "should only find unlocked and expired jobs" do
87
+ pending('Needs to work with forked workers')
88
+ @worker.name = 'worker3'
89
+ SimpleJob.runs.should == 0
90
+ @worker.work_off
91
+ SimpleJob.runs.should == 2 # runs the one open job and one expired job
92
+ end
93
+
94
+ it "should ignore locks when finding our own jobs" do
95
+ pending('Needs to work with forked workers')
96
+ @worker.name = 'worker1'
97
+ SimpleJob.runs.should == 0
98
+ @worker.work_off
99
+ SimpleJob.runs.should == 3 # runs open job plus worker1 jobs
100
+ # This is useful in the case of a crash/restart on worker1, but make sure multiple workers on the same host have unique names!
101
+ end
102
+
103
+ end
104
+
105
+ end