inst-jobs 0.11.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (86) hide show
  1. checksums.yaml +7 -0
  2. data/bin/inst_job +4 -0
  3. data/db/migrate/20101216224513_create_delayed_jobs.rb +40 -0
  4. data/db/migrate/20110208031356_add_delayed_jobs_tag.rb +14 -0
  5. data/db/migrate/20110426161613_add_delayed_jobs_max_attempts.rb +13 -0
  6. data/db/migrate/20110516225834_add_delayed_jobs_strand.rb +14 -0
  7. data/db/migrate/20110531144916_cleanup_delayed_jobs_indexes.rb +26 -0
  8. data/db/migrate/20110610213249_optimize_delayed_jobs.rb +40 -0
  9. data/db/migrate/20110831210257_add_delayed_jobs_next_in_strand.rb +52 -0
  10. data/db/migrate/20120510004759_delayed_jobs_delete_trigger_lock_for_update.rb +31 -0
  11. data/db/migrate/20120531150712_drop_psql_jobs_pop_fn.rb +15 -0
  12. data/db/migrate/20120607164022_delayed_jobs_use_advisory_locks.rb +80 -0
  13. data/db/migrate/20120607181141_index_jobs_on_locked_by.rb +15 -0
  14. data/db/migrate/20120608191051_add_jobs_run_at_index.rb +15 -0
  15. data/db/migrate/20120927184213_change_delayed_jobs_handler_to_text.rb +13 -0
  16. data/db/migrate/20140505215131_add_failed_jobs_original_job_id.rb +13 -0
  17. data/db/migrate/20140505215510_copy_failed_jobs_original_id.rb +13 -0
  18. data/db/migrate/20140505223637_drop_failed_jobs_original_id.rb +13 -0
  19. data/db/migrate/20140512213941_add_source_to_jobs.rb +15 -0
  20. data/db/migrate/20150807133223_add_max_concurrent_to_jobs.rb +70 -0
  21. data/db/migrate/20151123210429_add_expires_at_to_jobs.rb +15 -0
  22. data/db/migrate/20151210162949_improve_max_concurrent.rb +50 -0
  23. data/lib/delayed/backend/active_record.rb +340 -0
  24. data/lib/delayed/backend/base.rb +335 -0
  25. data/lib/delayed/backend/redis/bulk_update.lua +50 -0
  26. data/lib/delayed/backend/redis/destroy_job.lua +2 -0
  27. data/lib/delayed/backend/redis/enqueue.lua +29 -0
  28. data/lib/delayed/backend/redis/fail_job.lua +5 -0
  29. data/lib/delayed/backend/redis/find_available.lua +3 -0
  30. data/lib/delayed/backend/redis/functions.rb +57 -0
  31. data/lib/delayed/backend/redis/get_and_lock_next_available.lua +17 -0
  32. data/lib/delayed/backend/redis/includes/jobs_common.lua +203 -0
  33. data/lib/delayed/backend/redis/job.rb +497 -0
  34. data/lib/delayed/backend/redis/set_running.lua +5 -0
  35. data/lib/delayed/backend/redis/tickle_strand.lua +2 -0
  36. data/lib/delayed/batch.rb +56 -0
  37. data/lib/delayed/cli.rb +101 -0
  38. data/lib/delayed/daemon.rb +103 -0
  39. data/lib/delayed/engine.rb +4 -0
  40. data/lib/delayed/job_tracking.rb +31 -0
  41. data/lib/delayed/lifecycle.rb +90 -0
  42. data/lib/delayed/log_tailer.rb +22 -0
  43. data/lib/delayed/message_sending.rb +134 -0
  44. data/lib/delayed/performable_method.rb +52 -0
  45. data/lib/delayed/periodic.rb +85 -0
  46. data/lib/delayed/plugin.rb +22 -0
  47. data/lib/delayed/pool.rb +161 -0
  48. data/lib/delayed/server/helpers.rb +28 -0
  49. data/lib/delayed/server/public/css/app.css +12 -0
  50. data/lib/delayed/server/public/js/app.js +132 -0
  51. data/lib/delayed/server/views/index.erb +90 -0
  52. data/lib/delayed/server/views/layout.erb +47 -0
  53. data/lib/delayed/server.rb +120 -0
  54. data/lib/delayed/settings.rb +90 -0
  55. data/lib/delayed/testing.rb +32 -0
  56. data/lib/delayed/version.rb +3 -0
  57. data/lib/delayed/work_queue/in_process.rb +13 -0
  58. data/lib/delayed/work_queue/parent_process.rb +180 -0
  59. data/lib/delayed/worker.rb +234 -0
  60. data/lib/delayed/yaml_extensions.rb +109 -0
  61. data/lib/delayed_job.rb +46 -0
  62. data/lib/inst-jobs.rb +1 -0
  63. data/spec/active_record_job_spec.rb +246 -0
  64. data/spec/delayed/cli_spec.rb +23 -0
  65. data/spec/delayed/daemon_spec.rb +35 -0
  66. data/spec/delayed/server_spec.rb +63 -0
  67. data/spec/delayed/settings_spec.rb +32 -0
  68. data/spec/delayed/work_queue/in_process_spec.rb +31 -0
  69. data/spec/delayed/work_queue/parent_process_spec.rb +159 -0
  70. data/spec/delayed/worker_spec.rb +16 -0
  71. data/spec/gemfiles/32.gemfile +6 -0
  72. data/spec/gemfiles/40.gemfile +5 -0
  73. data/spec/gemfiles/41.gemfile +5 -0
  74. data/spec/gemfiles/42.gemfile +5 -0
  75. data/spec/migrate/20140924140513_add_story_table.rb +7 -0
  76. data/spec/redis_job_spec.rb +140 -0
  77. data/spec/sample_jobs.rb +28 -0
  78. data/spec/shared/delayed_batch.rb +85 -0
  79. data/spec/shared/delayed_method.rb +419 -0
  80. data/spec/shared/performable_method.rb +66 -0
  81. data/spec/shared/shared_backend.rb +819 -0
  82. data/spec/shared/testing.rb +48 -0
  83. data/spec/shared/worker.rb +378 -0
  84. data/spec/shared_jobs_specs.rb +15 -0
  85. data/spec/spec_helper.rb +97 -0
  86. metadata +390 -0
@@ -0,0 +1,819 @@
1
+ shared_examples_for 'a backend' do
2
+ def create_job(opts = {})
3
+ Delayed::Job.enqueue(SimpleJob.new, { :queue => nil }.merge(opts))
4
+ end
5
+
6
+ before do
7
+ SimpleJob.runs = 0
8
+ end
9
+
10
+ it "should set run_at automatically if not set" do
11
+ Delayed::Job.create(:payload_object => ErrorJob.new).run_at.should_not be_nil
12
+ end
13
+
14
+ it "should not set run_at automatically if already set" do
15
+ later = Delayed::Job.db_time_now + 5.minutes
16
+ Delayed::Job.create(:payload_object => ErrorJob.new, :run_at => later).run_at.should be_within(1).of(later)
17
+ end
18
+
19
+ it "should raise ArgumentError when handler doesn't respond_to :perform" do
20
+ lambda { Delayed::Job.enqueue(Object.new) }.should raise_error(ArgumentError)
21
+ end
22
+
23
+ it "should increase count after enqueuing items" do
24
+ Delayed::Job.enqueue SimpleJob.new
25
+ Delayed::Job.jobs_count(:current).should == 1
26
+ end
27
+
28
+ it "should be able to set priority when enqueuing items" do
29
+ @job = Delayed::Job.enqueue SimpleJob.new, :priority => 5
30
+ @job.priority.should == 5
31
+ end
32
+
33
+ it "should use the default priority when enqueuing items" do
34
+ Delayed::Job.default_priority = 0
35
+ @job = Delayed::Job.enqueue SimpleJob.new
36
+ @job.priority.should == 0
37
+ Delayed::Job.default_priority = 10
38
+ @job = Delayed::Job.enqueue SimpleJob.new
39
+ @job.priority.should == 10
40
+ Delayed::Job.default_priority = 0
41
+ end
42
+
43
+ it "should be able to set run_at when enqueuing items" do
44
+ later = Delayed::Job.db_time_now + 5.minutes
45
+ @job = Delayed::Job.enqueue SimpleJob.new, :priority => 5, :run_at => later
46
+ @job.run_at.should be_within(1).of(later)
47
+ end
48
+
49
+ it "should be able to set expires_at when enqueuing items" do
50
+ later = Delayed::Job.db_time_now + 1.day
51
+ @job = Delayed::Job.enqueue SimpleJob.new, :expires_at => later
52
+ @job.expires_at.should be_within(1).of(later)
53
+ end
54
+
55
+ it "should work with jobs in modules" do
56
+ M::ModuleJob.runs = 0
57
+ job = Delayed::Job.enqueue M::ModuleJob.new
58
+ lambda { job.invoke_job }.should change { M::ModuleJob.runs }.from(0).to(1)
59
+ end
60
+
61
+ it "should raise an DeserializationError when the job class is totally unknown" do
62
+ job = Delayed::Job.new :handler => "--- !ruby/object:JobThatDoesNotExist {}"
63
+ lambda { job.payload_object.perform }.should raise_error(Delayed::Backend::DeserializationError)
64
+ end
65
+
66
+ it "should try to load the class when it is unknown at the time of the deserialization" do
67
+ job = Delayed::Job.new :handler => "--- !ruby/object:JobThatDoesNotExist {}"
68
+ lambda { job.payload_object.perform }.should raise_error(Delayed::Backend::DeserializationError)
69
+ end
70
+
71
+ it "should try include the namespace when loading unknown objects" do
72
+ job = Delayed::Job.new :handler => "--- !ruby/object:Delayed::JobThatDoesNotExist {}"
73
+ lambda { job.payload_object.perform }.should raise_error(Delayed::Backend::DeserializationError)
74
+ end
75
+
76
+ it "should also try to load structs when they are unknown (raises TypeError)" do
77
+ job = Delayed::Job.new :handler => "--- !ruby/struct:JobThatDoesNotExist {}"
78
+ lambda { job.payload_object.perform }.should raise_error(Delayed::Backend::DeserializationError)
79
+ end
80
+
81
+ it "should try include the namespace when loading unknown structs" do
82
+ job = Delayed::Job.new :handler => "--- !ruby/struct:Delayed::JobThatDoesNotExist {}"
83
+ lambda { job.payload_object.perform }.should raise_error(Delayed::Backend::DeserializationError)
84
+ end
85
+
86
+ describe "find_available" do
87
+ it "should not find failed jobs" do
88
+ @job = create_job :attempts => 50
89
+ @job.fail!
90
+ Delayed::Job.find_available(5).should_not include(@job)
91
+ end
92
+
93
+ it "should not find jobs scheduled for the future" do
94
+ @job = create_job :run_at => (Delayed::Job.db_time_now + 1.minute)
95
+ Delayed::Job.find_available(5).should_not include(@job)
96
+ end
97
+
98
+ it "should not find jobs locked by another worker" do
99
+ @job = create_job
100
+ Delayed::Job.get_and_lock_next_available('other_worker').should == @job
101
+ Delayed::Job.find_available(5).should_not include(@job)
102
+ end
103
+
104
+ it "should find open jobs" do
105
+ @job = create_job
106
+ Delayed::Job.find_available(5).should include(@job)
107
+ end
108
+ end
109
+
110
+ context "when another worker is already performing an task, it" do
111
+
112
+ before :each do
113
+ @job = Delayed::Job.create :payload_object => SimpleJob.new
114
+ Delayed::Job.get_and_lock_next_available('worker1').should == @job
115
+ end
116
+
117
+ it "should not allow a second worker to get exclusive access" do
118
+ Delayed::Job.get_and_lock_next_available('worker2').should be_nil
119
+ end
120
+
121
+ it "should not be found by another worker" do
122
+ Delayed::Job.find_available(1).length.should == 0
123
+ end
124
+ end
125
+
126
+ context "#name" do
127
+ it "should be the class name of the job that was enqueued" do
128
+ Delayed::Job.create(:payload_object => ErrorJob.new ).name.should == 'ErrorJob'
129
+ end
130
+
131
+ it "should be the method that will be called if its a performable method object" do
132
+ @job = Story.send_later_enqueue_args(:create, no_delay: true)
133
+ @job.name.should == "Story.create"
134
+ end
135
+
136
+ it "should be the instance method that will be called if its a performable method object" do
137
+ @job = Story.create(:text => "...").send_later_enqueue_args(:save, no_delay: true)
138
+ @job.name.should == 'Story#save'
139
+ end
140
+ end
141
+
142
+ context "worker prioritization" do
143
+ it "should fetch jobs ordered by priority" do
144
+ 10.times { create_job :priority => rand(10) }
145
+ jobs = Delayed::Job.find_available(10)
146
+ jobs.size.should == 10
147
+ jobs.each_cons(2) do |a, b|
148
+ a.priority.should <= b.priority
149
+ end
150
+ end
151
+
152
+ it "should not find jobs lower than the given priority" do
153
+ job1 = create_job :priority => 5
154
+ found = Delayed::Job.get_and_lock_next_available('test1', Delayed::Settings.queue, 10, 20)
155
+ found.should be_nil
156
+ job2 = create_job :priority => 10
157
+ found = Delayed::Job.get_and_lock_next_available('test1', Delayed::Settings.queue, 10, 20)
158
+ found.should == job2
159
+ job3 = create_job :priority => 15
160
+ found = Delayed::Job.get_and_lock_next_available('test2', Delayed::Settings.queue, 10, 20)
161
+ found.should == job3
162
+ end
163
+
164
+ it "should not find jobs higher than the given priority" do
165
+ job1 = create_job :priority => 25
166
+ found = Delayed::Job.get_and_lock_next_available('test1', Delayed::Settings.queue, 10, 20)
167
+ found.should be_nil
168
+ job2 = create_job :priority => 20
169
+ found = Delayed::Job.get_and_lock_next_available('test1', Delayed::Settings.queue, 10, 20)
170
+ found.should == job2
171
+ job3 = create_job :priority => 15
172
+ found = Delayed::Job.get_and_lock_next_available('test2', Delayed::Settings.queue, 10, 20)
173
+ found.should == job3
174
+ end
175
+ end
176
+
177
+ context "clear_locks!" do
178
+ before do
179
+ @job = create_job(:locked_by => 'worker', :locked_at => Delayed::Job.db_time_now)
180
+ end
181
+
182
+ it "should clear locks for the given worker" do
183
+ Delayed::Job.clear_locks!('worker')
184
+ Delayed::Job.find_available(5).should include(@job)
185
+ end
186
+
187
+ it "should not clear locks for other workers" do
188
+ Delayed::Job.clear_locks!('worker1')
189
+ Delayed::Job.find_available(5).should_not include(@job)
190
+ end
191
+ end
192
+
193
+ context "unlock" do
194
+ before do
195
+ @job = create_job(:locked_by => 'worker', :locked_at => Delayed::Job.db_time_now)
196
+ end
197
+
198
+ it "should clear locks" do
199
+ @job.unlock
200
+ @job.locked_by.should be_nil
201
+ @job.locked_at.should be_nil
202
+ end
203
+ end
204
+
205
+ context "strands" do
206
+ it "should run strand jobs in strict order" do
207
+ job1 = create_job(:strand => 'myjobs')
208
+ job2 = create_job(:strand => 'myjobs')
209
+ Delayed::Job.get_and_lock_next_available('w1').should == job1
210
+ Delayed::Job.get_and_lock_next_available('w2').should == nil
211
+ job1.destroy
212
+ # update time since the failed lock pushed it forward
213
+ job2.run_at = 1.minute.ago
214
+ job2.save!
215
+ Delayed::Job.get_and_lock_next_available('w3').should == job2
216
+ Delayed::Job.get_and_lock_next_available('w4').should == nil
217
+ end
218
+
219
+ it "should fail to lock if an earlier job gets locked" do
220
+ job1 = create_job(:strand => 'myjobs')
221
+ job2 = create_job(:strand => 'myjobs')
222
+ Delayed::Job.find_available(2).should == [job1]
223
+ Delayed::Job.find_available(2).should == [job1]
224
+
225
+ # job1 gets locked by w1
226
+ Delayed::Job.get_and_lock_next_available('w1').should == job1
227
+
228
+ # normally w2 would now be able to lock job2, but strands prevent it
229
+ Delayed::Job.get_and_lock_next_available('w2').should be_nil
230
+
231
+ # now job1 is done
232
+ job1.destroy
233
+ # update time since the failed lock pushed it forward
234
+ job2.run_at = 1.minute.ago
235
+ job2.save!
236
+ Delayed::Job.get_and_lock_next_available('w2').should == job2
237
+ end
238
+
239
+ it "should keep strand jobs in order as they are rescheduled" do
240
+ job1 = create_job(:strand => 'myjobs')
241
+ job2 = create_job(:strand => 'myjobs')
242
+ job3 = create_job(:strand => 'myjobs')
243
+ Delayed::Job.get_and_lock_next_available('w1').should == job1
244
+ Delayed::Job.find_available(1).should == []
245
+ job1.destroy
246
+ Delayed::Job.find_available(1).should == [job2]
247
+ # move job2's time forward
248
+ job2.run_at = 1.second.ago
249
+ job2.save!
250
+ job3.run_at = 5.seconds.ago
251
+ job3.save!
252
+ # we should still get job2, not job3
253
+ Delayed::Job.get_and_lock_next_available('w1').should == job2
254
+ end
255
+
256
+ it "should allow to run the next job if a failed job is present" do
257
+ job1 = create_job(:strand => 'myjobs')
258
+ job2 = create_job(:strand => 'myjobs')
259
+ job1.fail!
260
+ Delayed::Job.get_and_lock_next_available('w1').should == job2
261
+ end
262
+
263
+ it "should not interfere with jobs with no strand" do
264
+ jobs = [create_job(:strand => nil), create_job(:strand => 'myjobs')]
265
+ locked = [Delayed::Job.get_and_lock_next_available('w1'),
266
+ Delayed::Job.get_and_lock_next_available('w2')]
267
+ jobs.should =~ locked
268
+ Delayed::Job.get_and_lock_next_available('w3').should == nil
269
+ end
270
+
271
+ it "should not interfere with jobs in other strands" do
272
+ jobs = [create_job(:strand => 'strand1'), create_job(:strand => 'strand2')]
273
+ locked = [Delayed::Job.get_and_lock_next_available('w1'),
274
+ Delayed::Job.get_and_lock_next_available('w2')]
275
+ jobs.should =~ locked
276
+ Delayed::Job.get_and_lock_next_available('w3').should == nil
277
+ end
278
+
279
+ it "should not find next jobs when given no priority" do
280
+ jobs = [create_job(:strand => 'strand1'), create_job(:strand => 'strand1')]
281
+ first = Delayed::Job.get_and_lock_next_available('w1', Delayed::Settings.queue, nil, nil)
282
+ second = Delayed::Job.get_and_lock_next_available('w2', Delayed::Settings.queue, nil, nil)
283
+ expect(first).to eq jobs.first
284
+ expect(second).to eq nil
285
+ end
286
+
287
+ context 'singleton' do
288
+ it "should create if there's no jobs on the strand" do
289
+ @job = create_job(:singleton => 'myjobs')
290
+ @job.should be_present
291
+ Delayed::Job.get_and_lock_next_available('w1').should == @job
292
+ end
293
+
294
+ it "should create if there's another job on the strand, but it's running" do
295
+ @job = create_job(:singleton => 'myjobs')
296
+ @job.should be_present
297
+ Delayed::Job.get_and_lock_next_available('w1').should == @job
298
+
299
+ @job2 = create_job(:singleton => 'myjobs')
300
+ @job.should be_present
301
+ @job2.should_not == @job
302
+ end
303
+
304
+ it "should not create if there's another non-running job on the strand" do
305
+ @job = create_job(:singleton => 'myjobs')
306
+ @job.should be_present
307
+
308
+ @job2 = create_job(:singleton => 'myjobs')
309
+ @job2.should == @job
310
+ end
311
+
312
+ it "should not create if there's a job running and one waiting on the strand" do
313
+ @job = create_job(:singleton => 'myjobs')
314
+ @job.should be_present
315
+ Delayed::Job.get_and_lock_next_available('w1').should == @job
316
+
317
+ @job2 = create_job(:singleton => 'myjobs')
318
+ @job2.should be_present
319
+ @job2.should_not == @job
320
+
321
+ @job3 = create_job(:singleton => 'myjobs')
322
+ @job3.should == @job2
323
+ end
324
+
325
+ it "should update existing job if new job is set to run sooner" do
326
+ job1 = create_job(singleton: 'myjobs', run_at: 1.hour.from_now)
327
+ job2 = create_job(singleton: 'myjobs')
328
+ job2.should == job1
329
+ # it should be scheduled to run immediately
330
+ Delayed::Job.get_and_lock_next_available('w1').should == job1
331
+ end
332
+ end
333
+ end
334
+
335
+ context "on hold" do
336
+ it "should hold/unhold jobs" do
337
+ job1 = create_job()
338
+ job1.hold!
339
+ Delayed::Job.get_and_lock_next_available('w1').should be_nil
340
+
341
+ job1.unhold!
342
+ Delayed::Job.get_and_lock_next_available('w1').should == job1
343
+ end
344
+ end
345
+
346
+ context "periodic jobs" do
347
+ before(:each) do
348
+ # make the periodic job get scheduled in the past
349
+ @cron_time = 10.minutes.ago
350
+ allow(Delayed::Periodic).to receive(:now).and_return(@cron_time)
351
+ Delayed::Periodic.scheduled = {}
352
+ Delayed::Periodic.cron('my SimpleJob', '*/5 * * * * *') do
353
+ Delayed::Job.enqueue(SimpleJob.new)
354
+ end
355
+ end
356
+
357
+ it "should schedule jobs if they aren't scheduled yet" do
358
+ Delayed::Job.jobs_count(:current).should == 0
359
+ Delayed::Periodic.perform_audit!
360
+ Delayed::Job.jobs_count(:current).should == 1
361
+ job = Delayed::Job.get_and_lock_next_available('test1')
362
+ job.tag.should == 'periodic: my SimpleJob'
363
+ job.payload_object.should == Delayed::Periodic.scheduled['my SimpleJob']
364
+ job.run_at.should >= @cron_time
365
+ job.run_at.should <= @cron_time + 6.minutes
366
+ job.strand.should == job.tag
367
+ end
368
+
369
+ it "should schedule jobs if there are only failed jobs on the queue" do
370
+ Delayed::Job.jobs_count(:current).should == 0
371
+ expect { Delayed::Periodic.perform_audit! }.to change { Delayed::Job.jobs_count(:current) }.by(1)
372
+ Delayed::Job.jobs_count(:current).should == 1
373
+ job = Delayed::Job.get_and_lock_next_available('test1')
374
+ job.fail!
375
+ expect { Delayed::Periodic.perform_audit! }.to change{ Delayed::Job.jobs_count(:current) }.by(1)
376
+ end
377
+
378
+ it "should not schedule jobs that are already scheduled" do
379
+ Delayed::Job.jobs_count(:current).should == 0
380
+ Delayed::Periodic.perform_audit!
381
+ Delayed::Job.jobs_count(:current).should == 1
382
+ job = Delayed::Job.find_available(1).first
383
+ Delayed::Periodic.perform_audit!
384
+ Delayed::Job.jobs_count(:current).should == 1
385
+ # verify that the same job still exists, it wasn't just replaced with a new one
386
+ job.should == Delayed::Job.find_available(1).first
387
+ end
388
+
389
+ it "should schedule the next job run after performing" do
390
+ Delayed::Job.jobs_count(:current).should == 0
391
+ Delayed::Periodic.perform_audit!
392
+ Delayed::Job.jobs_count(:current).should == 1
393
+ job = Delayed::Job.get_and_lock_next_available('test')
394
+ run_job(job)
395
+
396
+ job = Delayed::Job.get_and_lock_next_available('test1')
397
+ job.tag.should == 'SimpleJob#perform'
398
+
399
+ next_scheduled = Delayed::Job.get_and_lock_next_available('test2')
400
+ next_scheduled.tag.should == 'periodic: my SimpleJob'
401
+ next_scheduled.payload_object.should be_is_a(Delayed::Periodic)
402
+ end
403
+
404
+ it "should reject duplicate named jobs" do
405
+ proc { Delayed::Periodic.cron('my SimpleJob', '*/15 * * * * *') {} }.should raise_error(ArgumentError)
406
+ end
407
+
408
+ it "should handle jobs that are no longer scheduled" do
409
+ Delayed::Periodic.perform_audit!
410
+ Delayed::Periodic.scheduled = {}
411
+ job = Delayed::Job.get_and_lock_next_available('test')
412
+ run_job(job)
413
+ # shouldn't error, and the job should now be deleted
414
+ Delayed::Job.jobs_count(:current).should == 0
415
+ end
416
+
417
+ it "should allow overriding schedules using periodic_jobs.yml" do
418
+ change_setting(Delayed::Periodic, :overrides, { 'my ChangedJob' => '*/10 * * * * *' }) do
419
+ Delayed::Periodic.scheduled = {}
420
+ Delayed::Periodic.cron('my ChangedJob', '*/5 * * * * *') do
421
+ Delayed::Job.enqueue(SimpleJob.new)
422
+ end
423
+ Delayed::Periodic.scheduled['my ChangedJob'].cron.original.should == '*/10 * * * * *'
424
+ end
425
+ end
426
+
427
+ it "should fail if the override cron line is invalid" do
428
+ change_setting(Delayed::Periodic, :overrides, { 'my ChangedJob' => '*/10 * * * * * *' }) do # extra asterisk
429
+ Delayed::Periodic.scheduled = {}
430
+ expect { Delayed::Periodic.cron('my ChangedJob', '*/5 * * * * *') do
431
+ Delayed::Job.enqueue(SimpleJob.new)
432
+ end }.to raise_error(ArgumentError)
433
+ end
434
+
435
+ expect { Delayed::Periodic.add_overrides({ 'my ChangedJob' => '*/10 * * * * * *' }) }.to raise_error(ArgumentError)
436
+ end
437
+ end
438
+
439
+ module InDelayedJobTest
440
+ def self.check_in_job
441
+ Delayed::Job.in_delayed_job?.should == true
442
+ end
443
+ end
444
+
445
+ it "should set in_delayed_job?" do
446
+ job = InDelayedJobTest.send_later_enqueue_args(:check_in_job, no_delay: true)
447
+ Delayed::Job.in_delayed_job?.should == false
448
+ job.invoke_job
449
+ Delayed::Job.in_delayed_job?.should == false
450
+ end
451
+
452
+ it "should fail on job creation if an unsaved AR object is used" do
453
+ story = Story.new :text => "Once upon..."
454
+ lambda { story.send_later(:text) }.should raise_error(RuntimeError)
455
+
456
+ reader = StoryReader.new
457
+ lambda { reader.send_later(:read, story) }.should raise_error(RuntimeError)
458
+
459
+ lambda { [story, 1, story, false].send_later(:first) }.should raise_error(RuntimeError)
460
+ end
461
+
462
+ # the sort order of current_jobs and list_jobs depends on the back-end
463
+ # implementation, so sort order isn't tested in these specs
464
+ describe "current jobs, queue size, strand_size" do
465
+ before do
466
+ @jobs = []
467
+ 3.times { @jobs << create_job(:priority => 3) }
468
+ @jobs.unshift create_job(:priority => 2)
469
+ @jobs.unshift create_job(:priority => 1)
470
+ @jobs << create_job(:priority => 3, :strand => "test1")
471
+ @future_job = create_job(:run_at => 5.hours.from_now)
472
+ 2.times { @jobs << create_job(:priority => 3) }
473
+ @jobs << create_job(:priority => 3, :strand => "test1")
474
+ @failed_job = create_job.tap { |j| j.fail! }
475
+ @other_queue_job = create_job(:queue => "another")
476
+ end
477
+
478
+ it "should return the queued jobs" do
479
+ Delayed::Job.list_jobs(:current, 100).map(&:id).sort.should == @jobs.map(&:id).sort
480
+ end
481
+
482
+ it "should paginate the returned jobs" do
483
+ @returned = []
484
+ @returned += Delayed::Job.list_jobs(:current, 3, 0)
485
+ @returned += Delayed::Job.list_jobs(:current, 4, 3)
486
+ @returned += Delayed::Job.list_jobs(:current, 100, 7)
487
+ @returned.sort_by { |j| j.id }.should == @jobs.sort_by { |j| j.id }
488
+ end
489
+
490
+ it "should return other queues" do
491
+ Delayed::Job.list_jobs(:current, 5, 0, "another").should == [@other_queue_job]
492
+ end
493
+
494
+ it "should return queue size" do
495
+ Delayed::Job.jobs_count(:current).should == @jobs.size
496
+ Delayed::Job.jobs_count(:current, "another").should == 1
497
+ Delayed::Job.jobs_count(:current, "bogus").should == 0
498
+ end
499
+
500
+ it "should return strand size" do
501
+ Delayed::Job.strand_size("test1").should == 2
502
+ Delayed::Job.strand_size("bogus").should == 0
503
+ end
504
+ end
505
+
506
+ it "should return the jobs in a strand" do
507
+ strand_jobs = []
508
+ 3.times { strand_jobs << create_job(:strand => 'test1') }
509
+ 2.times { create_job(:strand => 'test2') }
510
+ strand_jobs << create_job(:strand => 'test1', :run_at => 5.hours.from_now)
511
+ create_job
512
+
513
+ jobs = Delayed::Job.list_jobs(:strand, 3, 0, "test1")
514
+ jobs.size.should == 3
515
+
516
+ jobs += Delayed::Job.list_jobs(:strand, 3, 3, "test1")
517
+ jobs.size.should == 4
518
+
519
+ jobs.sort_by { |j| j.id }.should == strand_jobs.sort_by { |j| j.id }
520
+ end
521
+
522
+ it "should return the jobs for a tag" do
523
+ tag_jobs = []
524
+ 3.times { tag_jobs << "test".send_later_enqueue_args(:to_s, :no_delay => true) }
525
+ 2.times { "test".send_later(:to_i) }
526
+ tag_jobs << "test".send_later_enqueue_args(:to_s, :run_at => 5.hours.from_now, :no_delay => true)
527
+ tag_jobs << "test".send_later_enqueue_args(:to_s, :strand => "test1", :no_delay => true)
528
+ "test".send_later_enqueue_args(:to_i, :strand => "test1")
529
+ create_job
530
+
531
+ jobs = Delayed::Job.list_jobs(:tag, 3, 0, "String#to_s")
532
+ jobs.size.should == 3
533
+
534
+ jobs += Delayed::Job.list_jobs(:tag, 3, 3, "String#to_s")
535
+ jobs.size.should == 5
536
+
537
+ jobs.sort_by { |j| j.id }.should == tag_jobs.sort_by { |j| j.id }
538
+ end
539
+
540
+ describe "running_jobs" do
541
+ it "should return the running jobs, ordered by locked_at" do
542
+ Timecop.freeze(10.minutes.ago) { 3.times { create_job } }
543
+ j1 = Timecop.freeze(2.minutes.ago) { Delayed::Job.get_and_lock_next_available('w1') }
544
+ j2 = Timecop.freeze(5.minutes.ago) { Delayed::Job.get_and_lock_next_available('w2') }
545
+ j3 = Timecop.freeze(5.seconds.ago) { Delayed::Job.get_and_lock_next_available('w3') }
546
+ [j1, j2, j3].compact.size.should == 3
547
+
548
+ Delayed::Job.running_jobs.should == [j2, j1, j3]
549
+ end
550
+ end
551
+
552
+ describe "future jobs" do
553
+ it "should find future jobs once their run_at rolls by" do
554
+ Timecop.freeze {
555
+ @job = create_job :run_at => 5.minutes.from_now
556
+ expect(Delayed::Job.find_available(5)).not_to include(@job)
557
+ }
558
+ Timecop.freeze(1.hour.from_now) {
559
+ expect(Delayed::Job.find_available(5)).to include(@job)
560
+ Delayed::Job.get_and_lock_next_available('test').should == @job
561
+ }
562
+ end
563
+
564
+ it "should return future jobs sorted by their run_at" do
565
+ @j1 = create_job
566
+ @j2 = create_job :run_at => 1.hour.from_now
567
+ @j3 = create_job :run_at => 30.minutes.from_now
568
+ Delayed::Job.list_jobs(:future, 1).should == [@j3]
569
+ Delayed::Job.list_jobs(:future, 5).should == [@j3, @j2]
570
+ Delayed::Job.list_jobs(:future, 1, 1).should == [@j2]
571
+ end
572
+ end
573
+
574
+ describe "failed jobs" do
575
+ # the sort order of failed_jobs depends on the back-end implementation,
576
+ # so sort order isn't tested here
577
+ it "should return the list of failed jobs" do
578
+ jobs = []
579
+ 3.times { jobs << create_job(:priority => 3) }
580
+ jobs = jobs.sort_by { |j| j.id }
581
+ Delayed::Job.list_jobs(:failed, 1).should == []
582
+ jobs[0].fail!
583
+ jobs[1].fail!
584
+ failed = (Delayed::Job.list_jobs(:failed, 1, 0) + Delayed::Job.list_jobs(:failed, 1, 1)).sort_by { |j| j.id }
585
+ failed.size.should == 2
586
+ failed[0].original_job_id.should == jobs[0].id
587
+ failed[1].original_job_id.should == jobs[1].id
588
+ end
589
+ end
590
+
591
+ describe "bulk_update" do
592
+ shared_examples_for "scope" do
593
+ before do
594
+ @affected_jobs = []
595
+ @ignored_jobs = []
596
+ end
597
+
598
+ it "should hold and unhold a scope of jobs" do
599
+ @affected_jobs.all? { |j| j.on_hold? }.should be false
600
+ @ignored_jobs.any? { |j| j.on_hold? }.should be false
601
+ Delayed::Job.bulk_update('hold', :flavor => @flavor, :query => @query).should == @affected_jobs.size
602
+
603
+ @affected_jobs.all? { |j| Delayed::Job.find(j.id).on_hold? }.should be true
604
+ @ignored_jobs.any? { |j| Delayed::Job.find(j.id).on_hold? }.should be false
605
+
606
+ # redis holding seems busted - it removes from the tag set and strand list, so you can't use a query
607
+ # to un-hold them
608
+ next if Delayed::Job == Delayed::Backend::Redis::Job
609
+ Delayed::Job.bulk_update('unhold', :flavor => @flavor, :query => @query).should == @affected_jobs.size
610
+
611
+ @affected_jobs.any? { |j| Delayed::Job.find(j.id).on_hold? }.should be false
612
+ @ignored_jobs.any? { |j| Delayed::Job.find(j.id).on_hold? }.should be false
613
+ end
614
+
615
+ it "should delete a scope of jobs" do
616
+ Delayed::Job.bulk_update('destroy', :flavor => @flavor, :query => @query).should == @affected_jobs.size
617
+ @affected_jobs.map { |j| Delayed::Job.find(j.id) rescue nil }.compact.should be_blank
618
+ @ignored_jobs.map { |j| Delayed::Job.find(j.id) rescue nil }.compact.size.should == @ignored_jobs.size
619
+ end
620
+ end
621
+
622
+ describe "scope: current" do
623
+ include_examples "scope"
624
+ before do
625
+ @flavor = 'current'
626
+ Timecop.freeze(5.minutes.ago) do
627
+ 3.times { @affected_jobs << create_job }
628
+ @ignored_jobs << create_job(:run_at => 2.hours.from_now)
629
+ @ignored_jobs << create_job(:queue => 'q2')
630
+ end
631
+ end
632
+ end
633
+
634
+ describe "scope: future" do
635
+ include_examples "scope"
636
+ before do
637
+ @flavor = 'future'
638
+ Timecop.freeze(5.minutes.ago) do
639
+ 3.times { @affected_jobs << create_job(:run_at => 2.hours.from_now) }
640
+ @ignored_jobs << create_job
641
+ @ignored_jobs << create_job(:queue => 'q2', :run_at => 2.hours.from_now)
642
+ end
643
+ end
644
+ end
645
+
646
+ describe "scope: strand" do
647
+ include_examples "scope"
648
+ before do
649
+ @flavor = 'strand'
650
+ @query = 's1'
651
+ Timecop.freeze(5.minutes.ago) do
652
+ @affected_jobs << create_job(:strand => 's1')
653
+ @affected_jobs << create_job(:strand => 's1', :run_at => 2.hours.from_now)
654
+ @ignored_jobs << create_job
655
+ @ignored_jobs << create_job(:strand => 's2')
656
+ @ignored_jobs << create_job(:strand => 's2', :run_at => 2.hours.from_now)
657
+ end
658
+ end
659
+ end
660
+
661
+ describe "scope: tag" do
662
+ include_examples "scope"
663
+ before do
664
+ @flavor = 'tag'
665
+ @query = 'String#to_i'
666
+ Timecop.freeze(5.minutes.ago) do
667
+ @affected_jobs << "test".send_later_enqueue_args(:to_i, :no_delay => true)
668
+ @affected_jobs << "test".send_later_enqueue_args(:to_i, :strand => 's1', :no_delay => true)
669
+ @affected_jobs << "test".send_later_enqueue_args(:to_i, :run_at => 2.hours.from_now, :no_delay => true)
670
+ @ignored_jobs << create_job
671
+ @ignored_jobs << create_job(:run_at => 1.hour.from_now)
672
+ end
673
+ end
674
+ end
675
+
676
+ it "should hold and un-hold given job ids" do
677
+ j1 = "test".send_later_enqueue_args(:to_i, :no_delay => true)
678
+ j2 = create_job(:run_at => 2.hours.from_now)
679
+ j3 = "test".send_later_enqueue_args(:to_i, :strand => 's1', :no_delay => true)
680
+ Delayed::Job.bulk_update('hold', :ids => [j1.id, j2.id]).should == 2
681
+ Delayed::Job.find(j1.id).on_hold?.should be true
682
+ Delayed::Job.find(j2.id).on_hold?.should be true
683
+ Delayed::Job.find(j3.id).on_hold?.should be false
684
+
685
+ Delayed::Job.bulk_update('unhold', :ids => [j2.id, j3.id]).should == 1
686
+ Delayed::Job.find(j1.id).on_hold?.should be true
687
+ Delayed::Job.find(j2.id).on_hold?.should be false
688
+ Delayed::Job.find(j3.id).on_hold?.should be false
689
+ end
690
+
691
+ it "should not hold locked jobs" do
692
+ job1 = Delayed::Job.new(:tag => 'tag')
693
+ job1.create_and_lock!("worker")
694
+ job1.on_hold?.should be false
695
+ Delayed::Job.bulk_update('hold', ids: [job1.id]).should == 0
696
+ Delayed::Job.find(job1.id).on_hold?.should be false
697
+ end
698
+
699
+ it "should not unhold locked jobs" do
700
+ job1 = Delayed::Job.new(:tag => 'tag')
701
+ job1.create_and_lock!("worker")
702
+ Delayed::Job.bulk_update('unhold', ids: [job1.id]).should == 0
703
+ Delayed::Job.find(job1.id).on_hold?.should be false
704
+ Delayed::Job.find(job1.id).locked?.should be true
705
+ end
706
+
707
+ it "should delete given job ids" do
708
+ jobs = (0..2).map { create_job }
709
+ Delayed::Job.bulk_update('destroy', :ids => jobs[0,2].map(&:id)).should == 2
710
+ jobs.map { |j| Delayed::Job.find(j.id) rescue nil }.compact.should == jobs[2,1]
711
+ end
712
+
713
+ it "should not delete locked jobs" do
714
+ job1 = Delayed::Job.new(:tag => 'tag')
715
+ job1.create_and_lock!("worker")
716
+ Delayed::Job.bulk_update('destroy', ids: [job1.id]).should == 0
717
+ Delayed::Job.find(job1.id).locked?.should be true
718
+ end
719
+ end
720
+
721
+ describe "tag_counts" do
722
+ before do
723
+ @cur = []
724
+ 3.times { @cur << "test".send_later_enqueue_args(:to_s, no_delay: true) }
725
+ 5.times { @cur << "test".send_later_enqueue_args(:to_i, no_delay: true) }
726
+ 2.times { @cur << "test".send_later_enqueue_args(:upcase, no_delay: true) }
727
+ ("test".send_later_enqueue_args :downcase, no_delay: true).fail!
728
+ @future = []
729
+ 5.times { @future << "test".send_later_enqueue_args(:downcase, run_at: 3.hours.from_now, no_delay: true) }
730
+ @cur << "test".send_later_enqueue_args(:downcase, no_delay: true)
731
+ end
732
+
733
+ it "should return a sorted list of popular current tags" do
734
+ Delayed::Job.tag_counts(:current, 1).should == [{ :tag => "String#to_i", :count => 5 }]
735
+ Delayed::Job.tag_counts(:current, 1, 1).should == [{ :tag => "String#to_s", :count => 3 }]
736
+ Delayed::Job.tag_counts(:current, 5).should == [{ :tag => "String#to_i", :count => 5 },
737
+ { :tag => "String#to_s", :count => 3 },
738
+ { :tag => "String#upcase", :count => 2 },
739
+ { :tag => "String#downcase", :count => 1 }]
740
+ @cur[0,4].each { |j| j.destroy }
741
+ @future[0].run_at = @future[1].run_at = 1.hour.ago
742
+ @future[0].save!
743
+ @future[1].save!
744
+
745
+ Delayed::Job.tag_counts(:current, 5).should == [{ :tag => "String#to_i", :count => 4 },
746
+ { :tag => "String#downcase", :count => 3 },
747
+ { :tag => "String#upcase", :count => 2 },]
748
+ end
749
+
750
+ it "should return a sorted list of all popular tags" do
751
+ Delayed::Job.tag_counts(:all, 1).should == [{ :tag => "String#downcase", :count => 6 }]
752
+ Delayed::Job.tag_counts(:all, 1, 1).should == [{ :tag => "String#to_i", :count => 5 }]
753
+ Delayed::Job.tag_counts(:all, 5).should == [{ :tag => "String#downcase", :count => 6 },
754
+ { :tag => "String#to_i", :count => 5 },
755
+ { :tag => "String#to_s", :count => 3 },
756
+ { :tag => "String#upcase", :count => 2 },]
757
+
758
+ @cur[0,4].each { |j| j.destroy }
759
+ @future[0].destroy
760
+ @future[1].fail!
761
+ @future[2].fail!
762
+
763
+ Delayed::Job.tag_counts(:all, 5).should == [{ :tag => "String#to_i", :count => 4 },
764
+ { :tag => "String#downcase", :count => 3 },
765
+ { :tag => "String#upcase", :count => 2 },]
766
+ end
767
+ end
768
+
769
+ it "should unlock orphaned jobs" do
770
+ change_setting(Delayed::Settings, :max_attempts, 2) do
771
+ job1 = Delayed::Job.new(:tag => 'tag')
772
+ job2 = Delayed::Job.new(:tag => 'tag')
773
+ job3 = Delayed::Job.new(:tag => 'tag')
774
+ job4 = Delayed::Job.new(:tag => 'tag')
775
+ job1.create_and_lock!("Jobworker:#{Process.pid}")
776
+ `echo ''`
777
+ child_pid = $?.pid
778
+ job2.create_and_lock!("Jobworker:#{child_pid}")
779
+ job3.create_and_lock!("someoneelse:#{Process.pid}")
780
+ job4.create_and_lock!("Jobworker:notanumber")
781
+
782
+ Delayed::Job.unlock_orphaned_jobs(nil, "Jobworker").should == 1
783
+
784
+ Delayed::Job.find(job1.id).locked_by.should_not be_nil
785
+ Delayed::Job.find(job2.id).locked_by.should be_nil
786
+ Delayed::Job.find(job3.id).locked_by.should_not be_nil
787
+ Delayed::Job.find(job4.id).locked_by.should_not be_nil
788
+
789
+ Delayed::Job.unlock_orphaned_jobs(nil, "Jobworker").should == 0
790
+ end
791
+ end
792
+
793
+ it "should unlock orphaned jobs given a pid" do
794
+ change_setting(Delayed::Settings, :max_attempts, 2) do
795
+ job1 = Delayed::Job.new(:tag => 'tag')
796
+ job2 = Delayed::Job.new(:tag => 'tag')
797
+ job3 = Delayed::Job.new(:tag => 'tag')
798
+ job4 = Delayed::Job.new(:tag => 'tag')
799
+ job1.create_and_lock!("Jobworker:#{Process.pid}")
800
+ `echo ''`
801
+ child_pid = $?.pid
802
+ `echo ''`
803
+ child_pid2 = $?.pid
804
+ job2.create_and_lock!("Jobworker:#{child_pid}")
805
+ job3.create_and_lock!("someoneelse:#{Process.pid}")
806
+ job4.create_and_lock!("Jobworker:notanumber")
807
+
808
+ Delayed::Job.unlock_orphaned_jobs(child_pid2, "Jobworker").should == 0
809
+ Delayed::Job.unlock_orphaned_jobs(child_pid, "Jobworker").should == 1
810
+
811
+ Delayed::Job.find(job1.id).locked_by.should_not be_nil
812
+ Delayed::Job.find(job2.id).locked_by.should be_nil
813
+ Delayed::Job.find(job3.id).locked_by.should_not be_nil
814
+ Delayed::Job.find(job4.id).locked_by.should_not be_nil
815
+
816
+ Delayed::Job.unlock_orphaned_jobs(child_pid, "Jobworker").should == 0
817
+ end
818
+ end
819
+ end