delayed_job_hooked 2.1.5

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,485 @@
1
+ require File.expand_path('../../../../spec/sample_jobs', __FILE__)
2
+
3
+ shared_examples_for 'a delayed_job backend' do
4
+ let(:worker) { Delayed::Worker.new }
5
+
6
+ def create_job(opts = {})
7
+ described_class.create(opts.merge(:payload_object => SimpleJob.new))
8
+ end
9
+
10
+ before do
11
+ Delayed::Worker.max_priority = nil
12
+ Delayed::Worker.min_priority = nil
13
+ Delayed::Worker.default_priority = 99
14
+ Delayed::Worker.delay_jobs = true
15
+ SimpleJob.runs = 0
16
+ described_class.delete_all
17
+ end
18
+
19
+ it "should set run_at automatically if not set" do
20
+ described_class.create(:payload_object => ErrorJob.new ).run_at.should_not be_nil
21
+ end
22
+
23
+ it "should not set run_at automatically if already set" do
24
+ later = described_class.db_time_now + 5.minutes
25
+ job = described_class.create(:payload_object => ErrorJob.new, :run_at => later)
26
+ job.run_at.should be_within(1).of(later)
27
+ end
28
+
29
+ describe "enqueue" do
30
+ context "with a hash" do
31
+ it "should raise ArgumentError when handler doesn't respond_to :perform" do
32
+ lambda { described_class.enqueue(:payload_object => Object.new) }.should raise_error(ArgumentError)
33
+ end
34
+
35
+ it "should be able to set priority" do
36
+ job = described_class.enqueue :payload_object => SimpleJob.new, :priority => 5
37
+ job.priority.should == 5
38
+ end
39
+
40
+ it "should use default priority" do
41
+ job = described_class.enqueue :payload_object => SimpleJob.new
42
+ job.priority.should == 99
43
+ end
44
+
45
+ it "should be able to set run_at" do
46
+ later = described_class.db_time_now + 5.minutes
47
+ job = described_class.enqueue :payload_object => SimpleJob.new, :run_at => later
48
+ job.run_at.should be_within(1).of(later)
49
+ end
50
+ end
51
+
52
+ context "with multiple arguments" do
53
+ it "should raise ArgumentError when handler doesn't respond_to :perform" do
54
+ lambda { described_class.enqueue(Object.new) }.should raise_error(ArgumentError)
55
+ end
56
+
57
+ it "should increase count after enqueuing items" do
58
+ described_class.enqueue SimpleJob.new
59
+ described_class.count.should == 1
60
+ end
61
+
62
+ it "should not increase count after enqueuing items when delay_jobs is false" do
63
+ Delayed::Worker.delay_jobs = false
64
+ described_class.enqueue SimpleJob.new
65
+ described_class.count.should == 0
66
+ end
67
+
68
+ it "should be able to set priority [DEPRECATED]" do
69
+ silence_warnings do
70
+ job = described_class.enqueue SimpleJob.new, 5
71
+ job.priority.should == 5
72
+ end
73
+ end
74
+
75
+ it "should use default priority when it is not set" do
76
+ @job = described_class.enqueue SimpleJob.new
77
+ @job.priority.should == 99
78
+ end
79
+
80
+ it "should be able to set run_at [DEPRECATED]" do
81
+ silence_warnings do
82
+ later = described_class.db_time_now + 5.minutes
83
+ @job = described_class.enqueue SimpleJob.new, 5, later
84
+ @job.run_at.should be_within(1).of(later)
85
+ end
86
+ end
87
+
88
+ it "should work with jobs in modules" do
89
+ M::ModuleJob.runs = 0
90
+ job = described_class.enqueue M::ModuleJob.new
91
+ lambda { job.invoke_job }.should change { M::ModuleJob.runs }.from(0).to(1)
92
+ end
93
+ end
94
+ end
95
+
96
+ describe "callbacks" do
97
+ before(:each) do
98
+ CallbackJob.messages = []
99
+ end
100
+
101
+ %w(before success after).each do |callback|
102
+ it "should call #{callback} with job" do
103
+ job = described_class.enqueue(CallbackJob.new)
104
+ job.payload_object.should_receive(callback).with(job)
105
+ job.invoke_job
106
+ end
107
+ end
108
+
109
+ it "should call before and after callbacks" do
110
+ job = described_class.enqueue(CallbackJob.new)
111
+ CallbackJob.messages.should == ["enqueue"]
112
+ job.invoke_job
113
+ CallbackJob.messages.should == ["enqueue", "before", "perform", "success", "after", "completed"]
114
+ end
115
+
116
+ it "should call the after callback with an error" do
117
+ job = described_class.enqueue(CallbackJob.new)
118
+ job.payload_object.should_receive(:perform).and_raise(RuntimeError.new("fail"))
119
+
120
+ lambda { job.invoke_job }.should raise_error
121
+ CallbackJob.messages.should == ["enqueue", "before", "error: RuntimeError", "after"]
122
+ end
123
+
124
+ it "should call error when before raises an error" do
125
+ job = described_class.enqueue(CallbackJob.new)
126
+ job.payload_object.should_receive(:before).and_raise(RuntimeError.new("fail"))
127
+ lambda { job.invoke_job }.should raise_error(RuntimeError)
128
+ CallbackJob.messages.should == ["enqueue", "error: RuntimeError", "after"]
129
+ end
130
+ end
131
+
132
+ describe "payload_object" do
133
+ it "should raise a DeserializationError when the job class is totally unknown" do
134
+ job = described_class.new :handler => "--- !ruby/object:JobThatDoesNotExist {}"
135
+ lambda { job.payload_object }.should raise_error(Delayed::DeserializationError)
136
+ end
137
+
138
+ it "should raise a DeserializationError when the job struct is totally unknown" do
139
+ job = described_class.new :handler => "--- !ruby/struct:StructThatDoesNotExist {}"
140
+ lambda { job.payload_object }.should raise_error(Delayed::DeserializationError)
141
+ end
142
+
143
+ it "should raise a DeserializationError when the YAML.load raises argument error" do
144
+ job = described_class.find(create_job.id)
145
+ YAML.should_receive(:load).and_raise(ArgumentError)
146
+ lambda { job.payload_object }.should raise_error(Delayed::DeserializationError)
147
+ end
148
+ end
149
+
150
+ describe "reserve" do
151
+ before do
152
+ Delayed::Worker.max_run_time = 2.minutes
153
+ end
154
+
155
+ it "should not reserve failed jobs" do
156
+ create_job :attempts => 50, :failed_at => described_class.db_time_now
157
+ described_class.reserve(worker).should be_nil
158
+ end
159
+
160
+ it "should not reserve jobs scheduled for the future" do
161
+ create_job :run_at => described_class.db_time_now + 1.minute
162
+ described_class.reserve(worker).should be_nil
163
+ end
164
+
165
+ it "should reserve jobs scheduled for the past" do
166
+ job = create_job :run_at => described_class.db_time_now - 1.minute
167
+ described_class.reserve(worker).should == job
168
+ end
169
+
170
+ it "should reserve jobs scheduled for the past when time zones are involved" do
171
+ Time.zone = 'US/Eastern'
172
+ job = create_job :run_at => described_class.db_time_now - 1.minute.ago.in_time_zone
173
+ described_class.reserve(worker).should == job
174
+ end
175
+
176
+ it "should not reserve jobs locked by other workers" do
177
+ job = create_job
178
+ other_worker = Delayed::Worker.new
179
+ other_worker.name = 'other_worker'
180
+ described_class.reserve(other_worker).should == job
181
+ described_class.reserve(worker).should be_nil
182
+ end
183
+
184
+ it "should reserve open jobs" do
185
+ job = create_job
186
+ described_class.reserve(worker).should == job
187
+ end
188
+
189
+ it "should reserve expired jobs" do
190
+ job = create_job(:locked_by => worker.name, :locked_at => described_class.db_time_now - 3.minutes)
191
+ described_class.reserve(worker).should == job
192
+ end
193
+
194
+ it "should reserve own jobs" do
195
+ job = create_job(:locked_by => worker.name, :locked_at => (described_class.db_time_now - 1.minutes))
196
+ described_class.reserve(worker).should == job
197
+ end
198
+ end
199
+
200
+ context "#name" do
201
+ it "should be the class name of the job that was enqueued" do
202
+ described_class.create(:payload_object => ErrorJob.new ).name.should == 'ErrorJob'
203
+ end
204
+
205
+ it "should be the method that will be called if its a performable method object" do
206
+ job = described_class.new(:payload_object => NamedJob.new)
207
+ job.name.should == 'named_job'
208
+ end
209
+
210
+ it "should be the instance method that will be called if its a performable method object" do
211
+ @job = Story.create(:text => "...").delay.save
212
+ @job.name.should == 'Story#save'
213
+ end
214
+
215
+ it "should parse from handler on deserialization error" do
216
+ job = Story.create(:text => "...").delay.text
217
+ job.payload_object.object.destroy
218
+ job = described_class.find(job.id)
219
+ job.name.should == 'Delayed::PerformableMethod'
220
+ end
221
+ end
222
+
223
+ context "worker prioritization" do
224
+ before(:each) do
225
+ Delayed::Worker.max_priority = nil
226
+ Delayed::Worker.min_priority = nil
227
+ end
228
+
229
+ it "should fetch jobs ordered by priority" do
230
+ 10.times { described_class.enqueue SimpleJob.new, :priority => rand(10) }
231
+ jobs = []
232
+ 10.times { jobs << described_class.reserve(worker) }
233
+ jobs.size.should == 10
234
+ jobs.each_cons(2) do |a, b|
235
+ a.priority.should <= b.priority
236
+ end
237
+ end
238
+
239
+ it "should only find jobs greater than or equal to min priority" do
240
+ min = 5
241
+ Delayed::Worker.min_priority = min
242
+ 10.times {|i| described_class.enqueue SimpleJob.new, :priority => i }
243
+ 5.times { described_class.reserve(worker).priority.should >= min }
244
+ end
245
+
246
+ it "should only find jobs less than or equal to max priority" do
247
+ max = 5
248
+ Delayed::Worker.max_priority = max
249
+ 10.times {|i| described_class.enqueue SimpleJob.new, :priority => i }
250
+ 5.times { described_class.reserve(worker).priority.should <= max }
251
+ end
252
+ end
253
+
254
+ context "clear_locks!" do
255
+ before do
256
+ @job = create_job(:locked_by => 'worker1', :locked_at => described_class.db_time_now)
257
+ end
258
+
259
+ it "should clear locks for the given worker" do
260
+ described_class.clear_locks!('worker1')
261
+ described_class.reserve(worker).should == @job
262
+ end
263
+
264
+ it "should not clear locks for other workers" do
265
+ described_class.clear_locks!('different_worker')
266
+ described_class.reserve(worker).should_not == @job
267
+ end
268
+ end
269
+
270
+ context "unlock" do
271
+ before do
272
+ @job = create_job(:locked_by => 'worker', :locked_at => described_class.db_time_now)
273
+ end
274
+
275
+ it "should clear locks" do
276
+ @job.unlock
277
+ @job.locked_by.should be_nil
278
+ @job.locked_at.should be_nil
279
+ end
280
+ end
281
+
282
+ context "large handler" do
283
+ before do
284
+ text = "Lorem ipsum dolor sit amet. " * 1000
285
+ @job = described_class.enqueue Delayed::PerformableMethod.new(text, :length, {})
286
+ end
287
+
288
+ it "should have an id" do
289
+ @job.id.should_not be_nil
290
+ end
291
+ end
292
+
293
+ context "max_attempts" do
294
+ before(:each) do
295
+ @job = described_class.enqueue SimpleJob.new
296
+ end
297
+
298
+ it 'should not be defined' do
299
+ @job.max_attempts.should be_nil
300
+ end
301
+
302
+ it 'should use the max_retries value on the payload when defined' do
303
+ @job.payload_object.stub!(:max_attempts).and_return(99)
304
+ @job.max_attempts.should == 99
305
+ end
306
+ end
307
+
308
+ describe "yaml serialization" do
309
+ it "should reload changed attributes" do
310
+ story = Story.create(:text => 'hello')
311
+ job = story.delay.tell
312
+ story.update_attributes :text => 'goodbye'
313
+ described_class.find(job.id).payload_object.object.text.should == 'goodbye'
314
+ end
315
+
316
+ it "should raise deserialization error for destroyed records" do
317
+ story = Story.create(:text => 'hello')
318
+ job = story.delay.tell
319
+ story.destroy
320
+ lambda {
321
+ described_class.find(job.id).payload_object
322
+ }.should raise_error(Delayed::DeserializationError)
323
+ end
324
+ end
325
+
326
+ describe "worker integration" do
327
+ before do
328
+ Delayed::Job.delete_all
329
+ SimpleJob.runs = 0
330
+ end
331
+
332
+ describe "running a job" do
333
+ it "should fail after Worker.max_run_time" do
334
+ begin
335
+ old_max_run_time = Delayed::Worker.max_run_time
336
+ Delayed::Worker.max_run_time = 1.second
337
+ @job = Delayed::Job.create :payload_object => LongRunningJob.new
338
+ worker.run(@job)
339
+ @job.reload.last_error.should =~ /expired/
340
+ @job.attempts.should == 1
341
+ ensure
342
+ Delayed::Worker.max_run_time = old_max_run_time
343
+ end
344
+ end
345
+
346
+ context "when the job raises a deserialization error" do
347
+ it "should mark the job as failed" do
348
+ Delayed::Worker.destroy_failed_jobs = false
349
+ job = described_class.create! :handler => "--- !ruby/object:JobThatDoesNotExist {}"
350
+ worker.work_off
351
+ job.reload
352
+ job.failed_at.should_not be_nil
353
+ end
354
+ end
355
+ end
356
+
357
+ describe "failed jobs" do
358
+ before do
359
+ # reset defaults
360
+ Delayed::Worker.destroy_failed_jobs = true
361
+ Delayed::Worker.max_attempts = 25
362
+
363
+ @job = Delayed::Job.enqueue(ErrorJob.new)
364
+ end
365
+
366
+ it "should record last_error when destroy_failed_jobs = false, max_attempts = 1" do
367
+ Delayed::Worker.destroy_failed_jobs = false
368
+ Delayed::Worker.max_attempts = 1
369
+ worker.run(@job)
370
+ @job.reload
371
+ @job.last_error.should =~ /did not work/
372
+ @job.attempts.should == 1
373
+ @job.failed_at.should_not be_nil
374
+ end
375
+
376
+ it "should re-schedule jobs after failing" do
377
+ worker.work_off
378
+ @job.reload
379
+ @job.last_error.should =~ /did not work/
380
+ @job.last_error.should =~ /sample_jobs.rb:\d+:in `perform'/
381
+ @job.attempts.should == 1
382
+ @job.run_at.should > Delayed::Job.db_time_now - 10.minutes
383
+ @job.run_at.should < Delayed::Job.db_time_now + 10.minutes
384
+ @job.locked_by.should be_nil
385
+ @job.locked_at.should be_nil
386
+ end
387
+
388
+ it 'should re-schedule with handler provided time if present' do
389
+ @job = Delayed::Job.enqueue(CustomRescheduleJob.new(99.minutes))
390
+ worker.run(@job)
391
+ @job.reload
392
+
393
+ (Delayed::Job.db_time_now + 99.minutes - @job.run_at).abs.should < 1
394
+ end
395
+
396
+ it "should not fail when the triggered error doesn't have a message" do
397
+ error_with_nil_message = StandardError.new
398
+ error_with_nil_message.stub!(:message).and_return nil
399
+ @job.stub!(:invoke_job).and_raise error_with_nil_message
400
+ lambda{worker.run(@job)}.should_not raise_error
401
+ end
402
+ end
403
+
404
+ context "reschedule" do
405
+ before do
406
+ @job = Delayed::Job.create :payload_object => SimpleJob.new
407
+ end
408
+
409
+ share_examples_for "any failure more than Worker.max_attempts times" do
410
+ context "when the job's payload has a #failure hook" do
411
+ before do
412
+ @job = Delayed::Job.create :payload_object => OnPermanentFailureJob.new
413
+ @job.payload_object.should respond_to :failure
414
+ end
415
+
416
+ it "should run that hook" do
417
+ @job.payload_object.should_receive :failure
418
+ worker.reschedule(@job)
419
+ end
420
+ end
421
+
422
+ context "when the job's payload has no #failure hook" do
423
+ # It's a little tricky to test this in a straightforward way,
424
+ # because putting a should_not_receive expectation on
425
+ # @job.payload_object.failure makes that object
426
+ # incorrectly return true to
427
+ # payload_object.respond_to? :failure, which is what
428
+ # reschedule uses to decide whether to call failure.
429
+ # So instead, we just make sure that the payload_object as it
430
+ # already stands doesn't respond_to? failure, then
431
+ # shove it through the iterated reschedule loop and make sure we
432
+ # don't get a NoMethodError (caused by calling that nonexistent
433
+ # failure method).
434
+
435
+ before do
436
+ @job.payload_object.should_not respond_to(:failure)
437
+ end
438
+
439
+ it "should not try to run that hook" do
440
+ lambda do
441
+ Delayed::Worker.max_attempts.times { worker.reschedule(@job) }
442
+ end.should_not raise_exception(NoMethodError)
443
+ end
444
+ end
445
+ end
446
+
447
+ context "and we want to destroy jobs" do
448
+ before do
449
+ Delayed::Worker.destroy_failed_jobs = true
450
+ end
451
+
452
+ it_should_behave_like "any failure more than Worker.max_attempts times"
453
+
454
+ it "should be destroyed if it failed more than Worker.max_attempts times" do
455
+ @job.should_receive(:destroy)
456
+ Delayed::Worker.max_attempts.times { worker.reschedule(@job) }
457
+ end
458
+
459
+ it "should not be destroyed if failed fewer than Worker.max_attempts times" do
460
+ @job.should_not_receive(:destroy)
461
+ (Delayed::Worker.max_attempts - 1).times { worker.reschedule(@job) }
462
+ end
463
+ end
464
+
465
+ context "and we don't want to destroy jobs" do
466
+ before do
467
+ Delayed::Worker.destroy_failed_jobs = false
468
+ end
469
+
470
+ it_should_behave_like "any failure more than Worker.max_attempts times"
471
+
472
+ it "should be failed if it failed more than Worker.max_attempts times" do
473
+ @job.reload.failed_at.should == nil
474
+ Delayed::Worker.max_attempts.times { worker.reschedule(@job) }
475
+ @job.reload.failed_at.should_not == nil
476
+ end
477
+
478
+ it "should not be failed if it failed fewer than Worker.max_attempts times" do
479
+ (Delayed::Worker.max_attempts - 1).times { worker.reschedule(@job) }
480
+ @job.reload.failed_at.should == nil
481
+ end
482
+ end
483
+ end
484
+ end
485
+ end