efficiency20-delayed_job 1.8.51

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,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 @@
1
+ require File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib', 'delayed', 'recipes'))
@@ -0,0 +1,43 @@
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
+ ActiveRecord::Base.default_timezone = :utc if Time.zone.nil?
15
+
16
+ ActiveRecord::Schema.define do
17
+
18
+ create_table :delayed_jobs, :force => true do |table|
19
+ table.integer :priority, :default => 0
20
+ table.integer :attempts, :default => 0
21
+ table.text :handler
22
+ table.string :last_error
23
+ table.datetime :run_at
24
+ table.datetime :locked_at
25
+ table.string :locked_by
26
+ table.datetime :failed_at
27
+ table.timestamps
28
+ end
29
+
30
+ create_table :stories, :force => true do |table|
31
+ table.string :text
32
+ end
33
+
34
+ end
35
+
36
+
37
+ # Purely useful for test cases...
38
+ class Story < ActiveRecord::Base
39
+ def tell; text; end
40
+ def whatever(n, _); tell*n; end
41
+
42
+ handle_asynchronously :whatever
43
+ end
@@ -0,0 +1,150 @@
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
+ context "send_at" do
129
+ it "should queue a new job" do
130
+ lambda do
131
+ "string".send_at(1.hour.from_now, :length)
132
+ end.should change { Delayed::Job.count }.by(1)
133
+ end
134
+
135
+ it "should schedule the job in the future" do
136
+ time = 1.hour.from_now
137
+ job = "string".send_at(time, :length)
138
+ job.run_at.should == time
139
+ end
140
+
141
+ it "should store payload as PerformableMethod" do
142
+ job = "string".send_at(1.hour.from_now, :count, 'r')
143
+ job.payload_object.class.should == Delayed::PerformableMethod
144
+ job.payload_object.method.should == :count
145
+ job.payload_object.args.should == ['r']
146
+ job.payload_object.perform.should == 1
147
+ end
148
+ end
149
+
150
+ end
@@ -0,0 +1,419 @@
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 = (Delayed::Job.db_time_now+5.minutes)
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 record last_error when destroy_failed_jobs = false, max_attempts = 1" do
115
+ Delayed::Job.destroy_failed_jobs = false
116
+ Delayed::Job::max_attempts = 1
117
+ job = Delayed::Job.enqueue ErrorJob.new
118
+ Delayed::Job.work_off
119
+ job.reload
120
+ job.last_error.should =~ /did not work/
121
+ job.last_error.should =~ /job_spec.rb/
122
+ job.attempts.should == 1
123
+
124
+ job.failed_at.should_not == nil
125
+ end
126
+
127
+ it "should raise an DeserializationError when the job class is totally unknown" do
128
+
129
+ job = Delayed::Job.new
130
+ job['handler'] = "--- !ruby/object:JobThatDoesNotExist {}"
131
+
132
+ lambda { job.payload_object.perform }.should raise_error(Delayed::DeserializationError)
133
+ end
134
+
135
+ it "should try to load the class when it is unknown at the time of the deserialization" do
136
+ job = Delayed::Job.new
137
+ job['handler'] = "--- !ruby/object:JobThatDoesNotExist {}"
138
+
139
+ job.should_receive(:attempt_to_load).with('JobThatDoesNotExist').and_return(true)
140
+
141
+ lambda { job.payload_object.perform }.should raise_error(Delayed::DeserializationError)
142
+ end
143
+
144
+ it "should try include the namespace when loading unknown objects" do
145
+ job = Delayed::Job.new
146
+ job['handler'] = "--- !ruby/object:Delayed::JobThatDoesNotExist {}"
147
+ job.should_receive(:attempt_to_load).with('Delayed::JobThatDoesNotExist').and_return(true)
148
+ lambda { job.payload_object.perform }.should raise_error(Delayed::DeserializationError)
149
+ end
150
+
151
+ it "should also try to load structs when they are unknown (raises TypeError)" do
152
+ job = Delayed::Job.new
153
+ job['handler'] = "--- !ruby/struct:JobThatDoesNotExist {}"
154
+
155
+ job.should_receive(:attempt_to_load).with('JobThatDoesNotExist').and_return(true)
156
+
157
+ lambda { job.payload_object.perform }.should raise_error(Delayed::DeserializationError)
158
+ end
159
+
160
+ it "should try include the namespace when loading unknown structs" do
161
+ job = Delayed::Job.new
162
+ job['handler'] = "--- !ruby/struct:Delayed::JobThatDoesNotExist {}"
163
+
164
+ job.should_receive(:attempt_to_load).with('Delayed::JobThatDoesNotExist').and_return(true)
165
+ lambda { job.payload_object.perform }.should raise_error(Delayed::DeserializationError)
166
+ end
167
+
168
+ context "reschedule" do
169
+ before do
170
+ @job = Delayed::Job.create :payload_object => SimpleJob.new
171
+ end
172
+
173
+ context "and we want to destroy jobs" do
174
+ before do
175
+ Delayed::Job.destroy_failed_jobs = true
176
+ end
177
+
178
+ it "should be destroyed if it failed more than Job::max_attempts times" do
179
+ @job.should_receive(:destroy)
180
+ Delayed::Job::max_attempts.times { @job.reschedule 'FAIL' }
181
+ end
182
+
183
+ it "should not be destroyed if failed fewer than Job::max_attempts times" do
184
+ @job.should_not_receive(:destroy)
185
+ (Delayed::Job::max_attempts - 1).times { @job.reschedule 'FAIL' }
186
+ end
187
+ end
188
+
189
+ context "and we don't want to destroy jobs" do
190
+ before do
191
+ Delayed::Job.destroy_failed_jobs = false
192
+ end
193
+
194
+ it "should be failed if it failed more than Job::max_attempts times" do
195
+ @job.reload.failed_at.should == nil
196
+ Delayed::Job::max_attempts.times { @job.reschedule 'FAIL' }
197
+ @job.reload.failed_at.should_not == nil
198
+ end
199
+
200
+ it "should not be failed if it failed fewer than Job::max_attempts times" do
201
+ (Delayed::Job::max_attempts - 1).times { @job.reschedule 'FAIL' }
202
+ @job.reload.failed_at.should == nil
203
+ end
204
+
205
+ end
206
+ end
207
+
208
+ it "should fail after Job::max_run_time" do
209
+ @job = Delayed::Job.create :payload_object => LongRunningJob.new
210
+ Delayed::Job.reserve_and_run_one_job(1.second)
211
+ @job.reload.last_error.should =~ /expired/
212
+ @job.attempts.should == 1
213
+ end
214
+
215
+ it "should never find failed jobs" do
216
+ @job = Delayed::Job.create :payload_object => SimpleJob.new, :attempts => 50, :failed_at => Delayed::Job.db_time_now
217
+ Delayed::Job.find_available(1).length.should == 0
218
+ end
219
+
220
+ context "when another worker is already performing an task, it" do
221
+
222
+ before :each do
223
+ Delayed::Job.worker_name = 'worker1'
224
+ @job = Delayed::Job.create :payload_object => SimpleJob.new, :locked_by => 'worker1', :locked_at => Delayed::Job.db_time_now - 5.minutes
225
+ end
226
+
227
+ it "should not allow a second worker to get exclusive access" do
228
+ @job.lock_exclusively!(4.hours, 'worker2').should == false
229
+ end
230
+
231
+ it "should allow a second worker to get exclusive access if the timeout has passed" do
232
+ @job.lock_exclusively!(1.minute, 'worker2').should == true
233
+ end
234
+
235
+ it "should be able to get access to the task if it was started more then max_age ago" do
236
+ @job.locked_at = 5.hours.ago
237
+ @job.save
238
+
239
+ @job.lock_exclusively! 4.hours, 'worker2'
240
+ @job.reload
241
+ @job.locked_by.should == 'worker2'
242
+ @job.locked_at.should > 1.minute.ago
243
+ end
244
+
245
+ it "should not be found by another worker" do
246
+ Delayed::Job.worker_name = 'worker2'
247
+
248
+ Delayed::Job.find_available(1, 6.minutes).length.should == 0
249
+ end
250
+
251
+ it "should be found by another worker if the time has expired" do
252
+ Delayed::Job.worker_name = 'worker2'
253
+
254
+ Delayed::Job.find_available(1, 4.minutes).length.should == 1
255
+ end
256
+
257
+ it "should be able to get exclusive access again when the worker name is the same" do
258
+ @job.lock_exclusively! 5.minutes, 'worker1'
259
+ @job.lock_exclusively! 5.minutes, 'worker1'
260
+ @job.lock_exclusively! 5.minutes, 'worker1'
261
+ end
262
+ end
263
+
264
+ context "when another worker has worked on a task since the job was found to be available, it" do
265
+
266
+ before :each do
267
+ Delayed::Job.worker_name = 'worker1'
268
+ @job = Delayed::Job.create :payload_object => SimpleJob.new
269
+ @job_copy_for_worker_2 = Delayed::Job.find(@job.id)
270
+ end
271
+
272
+ it "should not allow a second worker to get exclusive access if already successfully processed by worker1" do
273
+ @job.delete
274
+ @job_copy_for_worker_2.lock_exclusively!(4.hours, 'worker2').should == false
275
+ end
276
+
277
+ 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
278
+ @job.update_attributes(:attempts => 1, :run_at => 1.day.from_now)
279
+ @job_copy_for_worker_2.lock_exclusively!(4.hours, 'worker2').should == false
280
+ end
281
+ end
282
+
283
+ context "#name" do
284
+ it "should be the class name of the job that was enqueued" do
285
+ Delayed::Job.create(:payload_object => ErrorJob.new ).name.should == 'ErrorJob'
286
+ end
287
+
288
+ it "should be the method that will be called if its a performable method object" do
289
+ Delayed::Job.send_later(:clear_locks!)
290
+ Delayed::Job.last.name.should == 'Delayed::Job.clear_locks!'
291
+
292
+ end
293
+ it "should be the instance method that will be called if its a performable method object" do
294
+ story = Story.create :text => "..."
295
+
296
+ story.send_later(:save)
297
+
298
+ Delayed::Job.last.name.should == 'Story#save'
299
+ end
300
+ end
301
+
302
+ context "worker prioritization" do
303
+
304
+ before(:each) do
305
+ Delayed::Job.max_priority = nil
306
+ Delayed::Job.min_priority = nil
307
+ end
308
+
309
+ it "should only work_off jobs that are >= min_priority" do
310
+ Delayed::Job.min_priority = -5
311
+ Delayed::Job.max_priority = 5
312
+ SimpleJob.runs.should == 0
313
+
314
+ Delayed::Job.enqueue SimpleJob.new, -10
315
+ Delayed::Job.enqueue SimpleJob.new, 0
316
+ Delayed::Job.work_off
317
+
318
+ SimpleJob.runs.should == 1
319
+ end
320
+
321
+ it "should only work_off jobs that are <= max_priority" do
322
+ Delayed::Job.min_priority = -5
323
+ Delayed::Job.max_priority = 5
324
+ SimpleJob.runs.should == 0
325
+
326
+ Delayed::Job.enqueue SimpleJob.new, 10
327
+ Delayed::Job.enqueue SimpleJob.new, 0
328
+
329
+ Delayed::Job.work_off
330
+
331
+ SimpleJob.runs.should == 1
332
+ end
333
+
334
+ it "should fetch jobs ordered by priority" do
335
+ number_of_jobs = 10
336
+ number_of_jobs.times { Delayed::Job.enqueue SimpleJob.new, rand(10) }
337
+ jobs = Delayed::Job.find_available(10)
338
+ ordered = true
339
+ jobs[1..-1].each_index{ |i|
340
+ if (jobs[i].priority < jobs[i+1].priority)
341
+ ordered = false
342
+ break
343
+ end
344
+ }
345
+ ordered.should == true
346
+ end
347
+
348
+ end
349
+
350
+ context "when pulling jobs off the queue for processing, it" do
351
+ before(:each) do
352
+ @job = Delayed::Job.create(
353
+ :payload_object => SimpleJob.new,
354
+ :locked_by => 'worker1',
355
+ :locked_at => Delayed::Job.db_time_now - 5.minutes)
356
+ end
357
+
358
+ it "should leave the queue in a consistent state and not run the job if locking fails" do
359
+ SimpleJob.runs.should == 0
360
+ @job.stub!(:lock_exclusively!).with(any_args).once.and_return(false)
361
+ Delayed::Job.should_receive(:find_available).once.and_return([@job])
362
+ Delayed::Job.work_off(1)
363
+ SimpleJob.runs.should == 0
364
+ end
365
+
366
+ end
367
+
368
+ context "while running alongside other workers that locked jobs, it" do
369
+ before(:each) do
370
+ Delayed::Job.worker_name = 'worker1'
371
+ Delayed::Job.create(:payload_object => SimpleJob.new, :locked_by => 'worker1', :locked_at => (Delayed::Job.db_time_now - 1.minutes))
372
+ Delayed::Job.create(:payload_object => SimpleJob.new, :locked_by => 'worker2', :locked_at => (Delayed::Job.db_time_now - 1.minutes))
373
+ Delayed::Job.create(:payload_object => SimpleJob.new)
374
+ Delayed::Job.create(:payload_object => SimpleJob.new, :locked_by => 'worker1', :locked_at => (Delayed::Job.db_time_now - 1.minutes))
375
+ end
376
+
377
+ it "should ingore locked jobs from other workers" do
378
+ Delayed::Job.worker_name = 'worker3'
379
+ SimpleJob.runs.should == 0
380
+ Delayed::Job.work_off
381
+ SimpleJob.runs.should == 1 # runs the one open job
382
+ end
383
+
384
+ it "should find our own jobs regardless of locks" do
385
+ Delayed::Job.worker_name = 'worker1'
386
+ SimpleJob.runs.should == 0
387
+ Delayed::Job.work_off
388
+ SimpleJob.runs.should == 3 # runs open job plus worker1 jobs that were already locked
389
+ end
390
+ end
391
+
392
+ context "while running with locked and expired jobs, it" do
393
+ before(:each) do
394
+ Delayed::Job.worker_name = 'worker1'
395
+ exp_time = Delayed::Job.db_time_now - (1.minutes + Delayed::Job::max_run_time)
396
+ Delayed::Job.create(:payload_object => SimpleJob.new, :locked_by => 'worker1', :locked_at => exp_time)
397
+ Delayed::Job.create(:payload_object => SimpleJob.new, :locked_by => 'worker2', :locked_at => (Delayed::Job.db_time_now - 1.minutes))
398
+ Delayed::Job.create(:payload_object => SimpleJob.new)
399
+ Delayed::Job.create(:payload_object => SimpleJob.new, :locked_by => 'worker1', :locked_at => (Delayed::Job.db_time_now - 1.minutes))
400
+ end
401
+
402
+ it "should only find unlocked and expired jobs" do
403
+ Delayed::Job.worker_name = 'worker3'
404
+ SimpleJob.runs.should == 0
405
+ Delayed::Job.work_off
406
+ SimpleJob.runs.should == 2 # runs the one open job and one expired job
407
+ end
408
+
409
+ it "should ignore locks when finding our own jobs" do
410
+ Delayed::Job.worker_name = 'worker1'
411
+ SimpleJob.runs.should == 0
412
+ Delayed::Job.work_off
413
+ SimpleJob.runs.should == 3 # runs open job plus worker1 jobs
414
+ # This is useful in the case of a crash/restart on worker1, but make sure multiple workers on the same host have unique names!
415
+ end
416
+
417
+ end
418
+
419
+ end