delayed_job_unique_key 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/MIT-LICENSE +20 -0
- data/README.textile +246 -0
- data/contrib/delayed_job.monitrc +14 -0
- data/contrib/delayed_job_multiple.monitrc +23 -0
- data/lib/delayed/backend/base.rb +152 -0
- data/lib/delayed/backend/shared_spec.rb +566 -0
- data/lib/delayed/command.rb +101 -0
- data/lib/delayed/deserialization_error.rb +4 -0
- data/lib/delayed/lifecycle.rb +84 -0
- data/lib/delayed/message_sending.rb +54 -0
- data/lib/delayed/performable_mailer.rb +21 -0
- data/lib/delayed/performable_method.rb +33 -0
- data/lib/delayed/plugin.rb +15 -0
- data/lib/delayed/plugins/clear_locks.rb +15 -0
- data/lib/delayed/psych_ext.rb +75 -0
- data/lib/delayed/railtie.rb +16 -0
- data/lib/delayed/recipes.rb +50 -0
- data/lib/delayed/serialization/active_record.rb +19 -0
- data/lib/delayed/syck_ext.rb +34 -0
- data/lib/delayed/tasks.rb +11 -0
- data/lib/delayed/worker.rb +222 -0
- data/lib/delayed/yaml_ext.rb +10 -0
- data/lib/delayed_job.rb +22 -0
- data/lib/generators/delayed_job/delayed_job_generator.rb +11 -0
- data/lib/generators/delayed_job/templates/script +5 -0
- data/recipes/delayed_job.rb +1 -0
- data/spec/autoloaded/clazz.rb +7 -0
- data/spec/autoloaded/instance_clazz.rb +6 -0
- data/spec/autoloaded/instance_struct.rb +6 -0
- data/spec/autoloaded/struct.rb +7 -0
- data/spec/delayed/backend/test.rb +113 -0
- data/spec/delayed/serialization/test.rb +0 -0
- data/spec/fixtures/bad_alias.yml +1 -0
- data/spec/lifecycle_spec.rb +107 -0
- data/spec/message_sending_spec.rb +116 -0
- data/spec/performable_mailer_spec.rb +46 -0
- data/spec/performable_method_spec.rb +89 -0
- data/spec/sample_jobs.rb +75 -0
- data/spec/spec_helper.rb +45 -0
- data/spec/test_backend_spec.rb +13 -0
- data/spec/worker_spec.rb +19 -0
- data/spec/yaml_ext_spec.rb +41 -0
- metadata +197 -0
@@ -0,0 +1,566 @@
|
|
1
|
+
require File.expand_path('../../../../spec/sample_jobs', __FILE__)
|
2
|
+
|
3
|
+
require 'active_support/core_ext'
|
4
|
+
|
5
|
+
shared_examples_for 'a delayed_job backend' do
|
6
|
+
let(:worker) { Delayed::Worker.new }
|
7
|
+
|
8
|
+
def create_job(opts = {})
|
9
|
+
described_class.create(opts.merge(:payload_object => SimpleJob.new))
|
10
|
+
end
|
11
|
+
|
12
|
+
before do
|
13
|
+
Delayed::Worker.max_priority = nil
|
14
|
+
Delayed::Worker.min_priority = nil
|
15
|
+
Delayed::Worker.default_priority = 99
|
16
|
+
Delayed::Worker.delay_jobs = true
|
17
|
+
SimpleJob.runs = 0
|
18
|
+
described_class.delete_all
|
19
|
+
end
|
20
|
+
|
21
|
+
it "should set run_at automatically if not set" do
|
22
|
+
described_class.create(:payload_object => ErrorJob.new ).run_at.should_not be_nil
|
23
|
+
end
|
24
|
+
|
25
|
+
it "should not set run_at automatically if already set" do
|
26
|
+
later = described_class.db_time_now + 5.minutes
|
27
|
+
job = described_class.create(:payload_object => ErrorJob.new, :run_at => later)
|
28
|
+
job.run_at.should be_within(1).of(later)
|
29
|
+
end
|
30
|
+
|
31
|
+
describe "#reload" do
|
32
|
+
it 'should cause the payload to be reloaded' do
|
33
|
+
job = described_class.enqueue :payload_object => SimpleJob.new
|
34
|
+
job.payload_object.object_id.should_not == job.reload.payload_object.object_id
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
describe "enqueue" do
|
39
|
+
context "with a hash" do
|
40
|
+
it "should raise ArgumentError when handler doesn't respond_to :perform" do
|
41
|
+
lambda { described_class.enqueue(:payload_object => Object.new) }.should raise_error(ArgumentError)
|
42
|
+
end
|
43
|
+
|
44
|
+
it "should be able to set priority" do
|
45
|
+
job = described_class.enqueue :payload_object => SimpleJob.new, :priority => 5
|
46
|
+
job.priority.should == 5
|
47
|
+
end
|
48
|
+
|
49
|
+
it "should use default priority" do
|
50
|
+
job = described_class.enqueue :payload_object => SimpleJob.new
|
51
|
+
job.priority.should == 99
|
52
|
+
end
|
53
|
+
|
54
|
+
it "should be able to set run_at" do
|
55
|
+
later = described_class.db_time_now + 5.minutes
|
56
|
+
job = described_class.enqueue :payload_object => SimpleJob.new, :run_at => later
|
57
|
+
job.run_at.should be_within(1).of(later)
|
58
|
+
end
|
59
|
+
|
60
|
+
it "should be able to set queue" do
|
61
|
+
job = described_class.enqueue :payload_object => SimpleJob.new, :queue => 'tracking'
|
62
|
+
job.queue.should == 'tracking'
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
context "with multiple arguments" do
|
67
|
+
it "should raise ArgumentError when handler doesn't respond_to :perform" do
|
68
|
+
lambda { described_class.enqueue(Object.new) }.should raise_error(ArgumentError)
|
69
|
+
end
|
70
|
+
|
71
|
+
it "should increase count after enqueuing items" do
|
72
|
+
described_class.enqueue SimpleJob.new
|
73
|
+
described_class.count.should == 1
|
74
|
+
end
|
75
|
+
|
76
|
+
it "should be able to set priority [DEPRECATED]" do
|
77
|
+
silence_warnings do
|
78
|
+
job = described_class.enqueue SimpleJob.new, 5
|
79
|
+
job.priority.should == 5
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
it "should use default priority when it is not set" do
|
84
|
+
@job = described_class.enqueue SimpleJob.new
|
85
|
+
@job.priority.should == 99
|
86
|
+
end
|
87
|
+
|
88
|
+
it "should be able to set run_at [DEPRECATED]" do
|
89
|
+
silence_warnings do
|
90
|
+
later = described_class.db_time_now + 5.minutes
|
91
|
+
@job = described_class.enqueue SimpleJob.new, 5, later
|
92
|
+
@job.run_at.should be_within(1).of(later)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
it "should work with jobs in modules" do
|
97
|
+
M::ModuleJob.runs = 0
|
98
|
+
job = described_class.enqueue M::ModuleJob.new
|
99
|
+
lambda { job.invoke_job }.should change { M::ModuleJob.runs }.from(0).to(1)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
context "with delay_jobs = false" do
|
104
|
+
before(:each) do
|
105
|
+
Delayed::Worker.delay_jobs = false
|
106
|
+
end
|
107
|
+
|
108
|
+
it "should not increase count after enqueuing items" do
|
109
|
+
described_class.enqueue SimpleJob.new
|
110
|
+
described_class.count.should == 0
|
111
|
+
end
|
112
|
+
|
113
|
+
it 'should invoke the enqueued job' do
|
114
|
+
job = SimpleJob.new
|
115
|
+
job.should_receive(:perform)
|
116
|
+
described_class.enqueue job
|
117
|
+
end
|
118
|
+
|
119
|
+
it 'should return a job, not the result of invocation' do
|
120
|
+
described_class.enqueue(SimpleJob.new).should be_instance_of(described_class)
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
describe "callbacks" do
|
126
|
+
before(:each) do
|
127
|
+
CallbackJob.messages = []
|
128
|
+
end
|
129
|
+
|
130
|
+
%w(before success after).each do |callback|
|
131
|
+
it "should call #{callback} with job" do
|
132
|
+
job = described_class.enqueue(CallbackJob.new)
|
133
|
+
job.payload_object.should_receive(callback).with(job)
|
134
|
+
job.invoke_job
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
it "should call before and after callbacks" do
|
139
|
+
job = described_class.enqueue(CallbackJob.new)
|
140
|
+
CallbackJob.messages.should == ["enqueue"]
|
141
|
+
job.invoke_job
|
142
|
+
CallbackJob.messages.should == ["enqueue", "before", "perform", "success", "after"]
|
143
|
+
end
|
144
|
+
|
145
|
+
it "should call the after callback with an error" do
|
146
|
+
job = described_class.enqueue(CallbackJob.new)
|
147
|
+
job.payload_object.should_receive(:perform).and_raise(RuntimeError.new("fail"))
|
148
|
+
|
149
|
+
lambda { job.invoke_job }.should raise_error
|
150
|
+
CallbackJob.messages.should == ["enqueue", "before", "error: RuntimeError", "after"]
|
151
|
+
end
|
152
|
+
|
153
|
+
it "should call error when before raises an error" do
|
154
|
+
job = described_class.enqueue(CallbackJob.new)
|
155
|
+
job.payload_object.should_receive(:before).and_raise(RuntimeError.new("fail"))
|
156
|
+
lambda { job.invoke_job }.should raise_error(RuntimeError)
|
157
|
+
CallbackJob.messages.should == ["enqueue", "error: RuntimeError", "after"]
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
describe "payload_object" do
|
162
|
+
it "should raise a DeserializationError when the job class is totally unknown" do
|
163
|
+
job = described_class.new :handler => "--- !ruby/object:JobThatDoesNotExist {}"
|
164
|
+
lambda { job.payload_object }.should raise_error(Delayed::DeserializationError)
|
165
|
+
end
|
166
|
+
|
167
|
+
it "should raise a DeserializationError when the job struct is totally unknown" do
|
168
|
+
job = described_class.new :handler => "--- !ruby/struct:StructThatDoesNotExist {}"
|
169
|
+
lambda { job.payload_object }.should raise_error(Delayed::DeserializationError)
|
170
|
+
end
|
171
|
+
|
172
|
+
it "should raise a DeserializationError when the YAML.load raises argument error" do
|
173
|
+
job = described_class.new :handler => "--- !ruby/struct:GoingToRaiseArgError {}"
|
174
|
+
YAML.should_receive(:load).and_raise(ArgumentError)
|
175
|
+
lambda { job.payload_object }.should raise_error(Delayed::DeserializationError)
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
describe "reserve" do
|
180
|
+
before do
|
181
|
+
Delayed::Worker.max_run_time = 2.minutes
|
182
|
+
end
|
183
|
+
|
184
|
+
it "should not reserve failed jobs" do
|
185
|
+
create_job :attempts => 50, :failed_at => described_class.db_time_now
|
186
|
+
described_class.reserve(worker).should be_nil
|
187
|
+
end
|
188
|
+
|
189
|
+
it "should not reserve jobs scheduled for the future" do
|
190
|
+
create_job :run_at => described_class.db_time_now + 1.minute
|
191
|
+
described_class.reserve(worker).should be_nil
|
192
|
+
end
|
193
|
+
|
194
|
+
it "should reserve jobs scheduled for the past" do
|
195
|
+
job = create_job :run_at => described_class.db_time_now - 1.minute
|
196
|
+
described_class.reserve(worker).should == job
|
197
|
+
end
|
198
|
+
|
199
|
+
it "should reserve jobs scheduled for the past when time zones are involved" do
|
200
|
+
Time.zone = 'US/Eastern'
|
201
|
+
job = create_job :run_at => described_class.db_time_now - 1.minute
|
202
|
+
described_class.reserve(worker).should == job
|
203
|
+
end
|
204
|
+
|
205
|
+
it "should not reserve jobs locked by other workers" do
|
206
|
+
job = create_job
|
207
|
+
other_worker = Delayed::Worker.new
|
208
|
+
other_worker.name = 'other_worker'
|
209
|
+
described_class.reserve(other_worker).should == job
|
210
|
+
described_class.reserve(worker).should be_nil
|
211
|
+
end
|
212
|
+
|
213
|
+
it "should reserve open jobs" do
|
214
|
+
job = create_job
|
215
|
+
described_class.reserve(worker).should == job
|
216
|
+
end
|
217
|
+
|
218
|
+
it "should reserve expired jobs" do
|
219
|
+
job = create_job(:locked_by => 'some other worker', :locked_at => described_class.db_time_now - Delayed::Worker.max_run_time - 1.minute)
|
220
|
+
described_class.reserve(worker).should == job
|
221
|
+
end
|
222
|
+
|
223
|
+
it "should reserve own jobs" do
|
224
|
+
job = create_job(:locked_by => worker.name, :locked_at => (described_class.db_time_now - 1.minutes))
|
225
|
+
described_class.reserve(worker).should == job
|
226
|
+
end
|
227
|
+
end
|
228
|
+
|
229
|
+
context "#name" do
|
230
|
+
it "should be the class name of the job that was enqueued" do
|
231
|
+
described_class.create(:payload_object => ErrorJob.new ).name.should == 'ErrorJob'
|
232
|
+
end
|
233
|
+
|
234
|
+
it "should be the method that will be called if its a performable method object" do
|
235
|
+
job = described_class.new(:payload_object => NamedJob.new)
|
236
|
+
job.name.should == 'named_job'
|
237
|
+
end
|
238
|
+
|
239
|
+
it "should be the instance method that will be called if its a performable method object" do
|
240
|
+
@job = Story.create(:text => "...").delay.save
|
241
|
+
@job.name.should == 'Story#save'
|
242
|
+
end
|
243
|
+
|
244
|
+
it "should parse from handler on deserialization error" do
|
245
|
+
job = Story.create(:text => "...").delay.text
|
246
|
+
job.payload_object.object.destroy
|
247
|
+
job.reload.name.should == 'Delayed::PerformableMethod'
|
248
|
+
end
|
249
|
+
end
|
250
|
+
|
251
|
+
context "worker prioritization" do
|
252
|
+
before(:each) do
|
253
|
+
Delayed::Worker.max_priority = nil
|
254
|
+
Delayed::Worker.min_priority = nil
|
255
|
+
end
|
256
|
+
|
257
|
+
it "should fetch jobs ordered by priority" do
|
258
|
+
10.times { described_class.enqueue SimpleJob.new, :priority => rand(10) }
|
259
|
+
jobs = []
|
260
|
+
10.times { jobs << described_class.reserve(worker) }
|
261
|
+
jobs.size.should == 10
|
262
|
+
jobs.each_cons(2) do |a, b|
|
263
|
+
a.priority.should <= b.priority
|
264
|
+
end
|
265
|
+
end
|
266
|
+
|
267
|
+
it "should only find jobs greater than or equal to min priority" do
|
268
|
+
min = 5
|
269
|
+
Delayed::Worker.min_priority = min
|
270
|
+
10.times {|i| described_class.enqueue SimpleJob.new, :priority => i }
|
271
|
+
5.times { described_class.reserve(worker).priority.should >= min }
|
272
|
+
end
|
273
|
+
|
274
|
+
it "should only find jobs less than or equal to max priority" do
|
275
|
+
max = 5
|
276
|
+
Delayed::Worker.max_priority = max
|
277
|
+
10.times {|i| described_class.enqueue SimpleJob.new, :priority => i }
|
278
|
+
5.times { described_class.reserve(worker).priority.should <= max }
|
279
|
+
end
|
280
|
+
end
|
281
|
+
|
282
|
+
context "clear_locks!" do
|
283
|
+
before do
|
284
|
+
@job = create_job(:locked_by => 'worker1', :locked_at => described_class.db_time_now)
|
285
|
+
end
|
286
|
+
|
287
|
+
it "should clear locks for the given worker" do
|
288
|
+
described_class.clear_locks!('worker1')
|
289
|
+
described_class.reserve(worker).should == @job
|
290
|
+
end
|
291
|
+
|
292
|
+
it "should not clear locks for other workers" do
|
293
|
+
described_class.clear_locks!('different_worker')
|
294
|
+
described_class.reserve(worker).should_not == @job
|
295
|
+
end
|
296
|
+
end
|
297
|
+
|
298
|
+
context "unlock" do
|
299
|
+
before do
|
300
|
+
@job = create_job(:locked_by => 'worker', :locked_at => described_class.db_time_now)
|
301
|
+
end
|
302
|
+
|
303
|
+
it "should clear locks" do
|
304
|
+
@job.unlock
|
305
|
+
@job.locked_by.should be_nil
|
306
|
+
@job.locked_at.should be_nil
|
307
|
+
end
|
308
|
+
end
|
309
|
+
|
310
|
+
context "large handler" do
|
311
|
+
before do
|
312
|
+
text = "Lorem ipsum dolor sit amet. " * 1000
|
313
|
+
@job = described_class.enqueue Delayed::PerformableMethod.new(text, :length, {})
|
314
|
+
end
|
315
|
+
|
316
|
+
it "should have an id" do
|
317
|
+
@job.id.should_not be_nil
|
318
|
+
end
|
319
|
+
end
|
320
|
+
|
321
|
+
context "named queues" do
|
322
|
+
context "when worker has one queue set" do
|
323
|
+
before(:each) do
|
324
|
+
worker.queues = ['large']
|
325
|
+
end
|
326
|
+
|
327
|
+
it "should only work off jobs which are from its queue" do
|
328
|
+
SimpleJob.runs.should == 0
|
329
|
+
|
330
|
+
create_job(:queue => "large")
|
331
|
+
create_job(:queue => "small")
|
332
|
+
worker.work_off
|
333
|
+
|
334
|
+
SimpleJob.runs.should == 1
|
335
|
+
end
|
336
|
+
end
|
337
|
+
|
338
|
+
context "when worker has two queue set" do
|
339
|
+
before(:each) do
|
340
|
+
worker.queues = ['large', 'small']
|
341
|
+
end
|
342
|
+
|
343
|
+
it "should only work off jobs which are from its queue" do
|
344
|
+
SimpleJob.runs.should == 0
|
345
|
+
|
346
|
+
create_job(:queue => "large")
|
347
|
+
create_job(:queue => "small")
|
348
|
+
create_job(:queue => "medium")
|
349
|
+
create_job
|
350
|
+
worker.work_off
|
351
|
+
|
352
|
+
SimpleJob.runs.should == 2
|
353
|
+
end
|
354
|
+
end
|
355
|
+
|
356
|
+
context "when worker does not have queue set" do
|
357
|
+
before(:each) do
|
358
|
+
worker.queues = []
|
359
|
+
end
|
360
|
+
|
361
|
+
it "should work off all jobs" do
|
362
|
+
SimpleJob.runs.should == 0
|
363
|
+
|
364
|
+
create_job(:queue => "one")
|
365
|
+
create_job(:queue => "two")
|
366
|
+
create_job
|
367
|
+
worker.work_off
|
368
|
+
|
369
|
+
SimpleJob.runs.should == 3
|
370
|
+
end
|
371
|
+
end
|
372
|
+
end
|
373
|
+
|
374
|
+
context "max_attempts" do
|
375
|
+
before(:each) do
|
376
|
+
@job = described_class.enqueue SimpleJob.new
|
377
|
+
end
|
378
|
+
|
379
|
+
it 'should not be defined' do
|
380
|
+
@job.max_attempts.should be_nil
|
381
|
+
end
|
382
|
+
|
383
|
+
it 'should use the max_retries value on the payload when defined' do
|
384
|
+
@job.payload_object.stub!(:max_attempts).and_return(99)
|
385
|
+
@job.max_attempts.should == 99
|
386
|
+
end
|
387
|
+
end
|
388
|
+
|
389
|
+
describe "yaml serialization" do
|
390
|
+
it "should reload changed attributes" do
|
391
|
+
story = Story.create(:text => 'hello')
|
392
|
+
job = story.delay.tell
|
393
|
+
story.update_attributes :text => 'goodbye'
|
394
|
+
job.reload.payload_object.object.text.should == 'goodbye'
|
395
|
+
end
|
396
|
+
|
397
|
+
it "should raise deserialization error for destroyed records" do
|
398
|
+
story = Story.create(:text => 'hello')
|
399
|
+
job = story.delay.tell
|
400
|
+
story.destroy
|
401
|
+
lambda {
|
402
|
+
job.reload.payload_object
|
403
|
+
}.should raise_error(Delayed::DeserializationError)
|
404
|
+
end
|
405
|
+
end
|
406
|
+
|
407
|
+
describe "worker integration" do
|
408
|
+
before do
|
409
|
+
Delayed::Job.delete_all
|
410
|
+
SimpleJob.runs = 0
|
411
|
+
end
|
412
|
+
|
413
|
+
describe "running a job" do
|
414
|
+
it "should fail after Worker.max_run_time" do
|
415
|
+
begin
|
416
|
+
old_max_run_time = Delayed::Worker.max_run_time
|
417
|
+
Delayed::Worker.max_run_time = 1.second
|
418
|
+
@job = Delayed::Job.create :payload_object => LongRunningJob.new
|
419
|
+
worker.run(@job)
|
420
|
+
@job.reload.last_error.should =~ /expired/
|
421
|
+
@job.attempts.should == 1
|
422
|
+
ensure
|
423
|
+
Delayed::Worker.max_run_time = old_max_run_time
|
424
|
+
end
|
425
|
+
end
|
426
|
+
|
427
|
+
context "when the job raises a deserialization error" do
|
428
|
+
it "should mark the job as failed" do
|
429
|
+
Delayed::Worker.destroy_failed_jobs = false
|
430
|
+
job = described_class.create! :handler => "--- !ruby/object:JobThatDoesNotExist {}"
|
431
|
+
worker.work_off
|
432
|
+
job.reload
|
433
|
+
job.failed_at.should_not be_nil
|
434
|
+
end
|
435
|
+
end
|
436
|
+
end
|
437
|
+
|
438
|
+
describe "failed jobs" do
|
439
|
+
before do
|
440
|
+
# reset defaults
|
441
|
+
Delayed::Worker.destroy_failed_jobs = true
|
442
|
+
Delayed::Worker.max_attempts = 25
|
443
|
+
|
444
|
+
@job = Delayed::Job.enqueue(ErrorJob.new)
|
445
|
+
end
|
446
|
+
|
447
|
+
it "should record last_error when destroy_failed_jobs = false, max_attempts = 1" do
|
448
|
+
Delayed::Worker.destroy_failed_jobs = false
|
449
|
+
Delayed::Worker.max_attempts = 1
|
450
|
+
worker.run(@job)
|
451
|
+
@job.reload
|
452
|
+
@job.last_error.should =~ /did not work/
|
453
|
+
@job.attempts.should == 1
|
454
|
+
@job.failed_at.should_not be_nil
|
455
|
+
end
|
456
|
+
|
457
|
+
it "should re-schedule jobs after failing" do
|
458
|
+
worker.work_off
|
459
|
+
@job.reload
|
460
|
+
@job.last_error.should =~ /did not work/
|
461
|
+
@job.last_error.should =~ /sample_jobs.rb:\d+:in `perform'/
|
462
|
+
@job.attempts.should == 1
|
463
|
+
@job.run_at.should > Delayed::Job.db_time_now - 10.minutes
|
464
|
+
@job.run_at.should < Delayed::Job.db_time_now + 10.minutes
|
465
|
+
@job.locked_by.should be_nil
|
466
|
+
@job.locked_at.should be_nil
|
467
|
+
end
|
468
|
+
|
469
|
+
it 'should re-schedule with handler provided time if present' do
|
470
|
+
@job = Delayed::Job.enqueue(CustomRescheduleJob.new(99.minutes))
|
471
|
+
worker.run(@job)
|
472
|
+
@job.reload
|
473
|
+
|
474
|
+
(Delayed::Job.db_time_now + 99.minutes - @job.run_at).abs.should < 1
|
475
|
+
end
|
476
|
+
|
477
|
+
it "should not fail when the triggered error doesn't have a message" do
|
478
|
+
error_with_nil_message = StandardError.new
|
479
|
+
error_with_nil_message.stub!(:message).and_return nil
|
480
|
+
@job.stub!(:invoke_job).and_raise error_with_nil_message
|
481
|
+
lambda{worker.run(@job)}.should_not raise_error
|
482
|
+
end
|
483
|
+
end
|
484
|
+
|
485
|
+
context "reschedule" do
|
486
|
+
before do
|
487
|
+
@job = Delayed::Job.create :payload_object => SimpleJob.new
|
488
|
+
end
|
489
|
+
|
490
|
+
share_examples_for "any failure more than Worker.max_attempts times" do
|
491
|
+
context "when the job's payload has a #failure hook" do
|
492
|
+
before do
|
493
|
+
@job = Delayed::Job.create :payload_object => OnPermanentFailureJob.new
|
494
|
+
@job.payload_object.should respond_to :failure
|
495
|
+
end
|
496
|
+
|
497
|
+
it "should run that hook" do
|
498
|
+
@job.payload_object.should_receive :failure
|
499
|
+
worker.reschedule(@job)
|
500
|
+
end
|
501
|
+
end
|
502
|
+
|
503
|
+
context "when the job's payload has no #failure hook" do
|
504
|
+
# It's a little tricky to test this in a straightforward way,
|
505
|
+
# because putting a should_not_receive expectation on
|
506
|
+
# @job.payload_object.failure makes that object
|
507
|
+
# incorrectly return true to
|
508
|
+
# payload_object.respond_to? :failure, which is what
|
509
|
+
# reschedule uses to decide whether to call failure.
|
510
|
+
# So instead, we just make sure that the payload_object as it
|
511
|
+
# already stands doesn't respond_to? failure, then
|
512
|
+
# shove it through the iterated reschedule loop and make sure we
|
513
|
+
# don't get a NoMethodError (caused by calling that nonexistent
|
514
|
+
# failure method).
|
515
|
+
|
516
|
+
before do
|
517
|
+
@job.payload_object.should_not respond_to(:failure)
|
518
|
+
end
|
519
|
+
|
520
|
+
it "should not try to run that hook" do
|
521
|
+
lambda do
|
522
|
+
Delayed::Worker.max_attempts.times { worker.reschedule(@job) }
|
523
|
+
end.should_not raise_exception(NoMethodError)
|
524
|
+
end
|
525
|
+
end
|
526
|
+
end
|
527
|
+
|
528
|
+
context "and we want to destroy jobs" do
|
529
|
+
before do
|
530
|
+
Delayed::Worker.destroy_failed_jobs = true
|
531
|
+
end
|
532
|
+
|
533
|
+
it_should_behave_like "any failure more than Worker.max_attempts times"
|
534
|
+
|
535
|
+
it "should be destroyed if it failed more than Worker.max_attempts times" do
|
536
|
+
@job.should_receive(:destroy)
|
537
|
+
Delayed::Worker.max_attempts.times { worker.reschedule(@job) }
|
538
|
+
end
|
539
|
+
|
540
|
+
it "should not be destroyed if failed fewer than Worker.max_attempts times" do
|
541
|
+
@job.should_not_receive(:destroy)
|
542
|
+
(Delayed::Worker.max_attempts - 1).times { worker.reschedule(@job) }
|
543
|
+
end
|
544
|
+
end
|
545
|
+
|
546
|
+
context "and we don't want to destroy jobs" do
|
547
|
+
before do
|
548
|
+
Delayed::Worker.destroy_failed_jobs = false
|
549
|
+
end
|
550
|
+
|
551
|
+
it_should_behave_like "any failure more than Worker.max_attempts times"
|
552
|
+
|
553
|
+
it "should be failed if it failed more than Worker.max_attempts times" do
|
554
|
+
@job.reload.failed_at.should == nil
|
555
|
+
Delayed::Worker.max_attempts.times { worker.reschedule(@job) }
|
556
|
+
@job.reload.failed_at.should_not == nil
|
557
|
+
end
|
558
|
+
|
559
|
+
it "should not be failed if it failed fewer than Worker.max_attempts times" do
|
560
|
+
(Delayed::Worker.max_attempts - 1).times { worker.reschedule(@job) }
|
561
|
+
@job.reload.failed_at.should == nil
|
562
|
+
end
|
563
|
+
end
|
564
|
+
end
|
565
|
+
end
|
566
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
require 'daemons'
|
2
|
+
require 'optparse'
|
3
|
+
|
4
|
+
module Delayed
|
5
|
+
class Command
|
6
|
+
attr_accessor :worker_count
|
7
|
+
|
8
|
+
def initialize(args)
|
9
|
+
@options = {
|
10
|
+
:quiet => true,
|
11
|
+
:pid_dir => "#{Rails.root}/tmp/pids"
|
12
|
+
}
|
13
|
+
|
14
|
+
@worker_count = 1
|
15
|
+
@monitor = false
|
16
|
+
|
17
|
+
opts = OptionParser.new do |opts|
|
18
|
+
opts.banner = "Usage: #{File.basename($0)} [options] start|stop|restart|run"
|
19
|
+
|
20
|
+
opts.on('-h', '--help', 'Show this message') do
|
21
|
+
puts opts
|
22
|
+
exit 1
|
23
|
+
end
|
24
|
+
opts.on('-e', '--environment=NAME', 'Specifies the environment to run this delayed jobs under (test/development/production).') do |e|
|
25
|
+
STDERR.puts "The -e/--environment option has been deprecated and has no effect. Use RAILS_ENV and see http://github.com/collectiveidea/delayed_job/issues/#issue/7"
|
26
|
+
end
|
27
|
+
opts.on('--min-priority N', 'Minimum priority of jobs to run.') do |n|
|
28
|
+
@options[:min_priority] = n
|
29
|
+
end
|
30
|
+
opts.on('--max-priority N', 'Maximum priority of jobs to run.') do |n|
|
31
|
+
@options[:max_priority] = n
|
32
|
+
end
|
33
|
+
opts.on('-n', '--number_of_workers=workers', "Number of unique workers to spawn") do |worker_count|
|
34
|
+
@worker_count = worker_count.to_i rescue 1
|
35
|
+
end
|
36
|
+
opts.on('--pid-dir=DIR', 'Specifies an alternate directory in which to store the process ids.') do |dir|
|
37
|
+
@options[:pid_dir] = dir
|
38
|
+
end
|
39
|
+
opts.on('-i', '--identifier=n', 'A numeric identifier for the worker.') do |n|
|
40
|
+
@options[:identifier] = n
|
41
|
+
end
|
42
|
+
opts.on('-m', '--monitor', 'Start monitor process.') do
|
43
|
+
@monitor = true
|
44
|
+
end
|
45
|
+
opts.on('--sleep-delay N', "Amount of time to sleep when no jobs are found") do |n|
|
46
|
+
@options[:sleep_delay] = n
|
47
|
+
end
|
48
|
+
opts.on('-p', '--prefix NAME', "String to be prefixed to worker process names") do |prefix|
|
49
|
+
@options[:prefix] = prefix
|
50
|
+
end
|
51
|
+
opts.on('--queues=queues', "Specify which queue DJ must look up for jobs") do |queues|
|
52
|
+
@options[:queues] = queues.split(',')
|
53
|
+
end
|
54
|
+
opts.on('--queue=queue', "Specify which queue DJ must look up for jobs") do |queue|
|
55
|
+
@options[:queues] = queue.split(',')
|
56
|
+
end
|
57
|
+
end
|
58
|
+
@args = opts.parse!(args)
|
59
|
+
end
|
60
|
+
|
61
|
+
def daemonize
|
62
|
+
dir = @options[:pid_dir]
|
63
|
+
Dir.mkdir(dir) unless File.exists?(dir)
|
64
|
+
|
65
|
+
if @worker_count > 1 && @options[:identifier]
|
66
|
+
raise ArgumentError, 'Cannot specify both --number-of-workers and --identifier'
|
67
|
+
elsif @worker_count == 1 && @options[:identifier]
|
68
|
+
process_name = "delayed_job.#{@options[:identifier]}"
|
69
|
+
run_process(process_name, dir)
|
70
|
+
else
|
71
|
+
worker_count.times do |worker_index|
|
72
|
+
process_name = worker_count == 1 ? "delayed_job" : "delayed_job.#{worker_index}"
|
73
|
+
run_process(process_name, dir)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def run_process(process_name, dir)
|
79
|
+
Delayed::Worker.before_fork
|
80
|
+
Daemons.run_proc(process_name, :dir => dir, :dir_mode => :normal, :monitor => @monitor, :ARGV => @args) do |*args|
|
81
|
+
$0 = File.join(@options[:prefix], process_name) if @options[:prefix]
|
82
|
+
run process_name
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def run(worker_name = nil)
|
87
|
+
Dir.chdir(Rails.root)
|
88
|
+
|
89
|
+
Delayed::Worker.after_fork
|
90
|
+
Delayed::Worker.logger = Logger.new(File.join(Rails.root, 'log', 'delayed_job.log'))
|
91
|
+
|
92
|
+
worker = Delayed::Worker.new(@options)
|
93
|
+
worker.name_prefix = "#{worker_name} "
|
94
|
+
worker.start
|
95
|
+
rescue => e
|
96
|
+
Rails.logger.fatal e
|
97
|
+
STDERR.puts e.message
|
98
|
+
exit 1
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|