mathie-delayed_job 1.8.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,406 @@
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 = 5.minutes.from_now
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 raise an DeserializationError when the job class is totally unknown" do
115
+
116
+ job = Delayed::Job.new
117
+ job['handler'] = "--- !ruby/object:JobThatDoesNotExist {}"
118
+
119
+ lambda { job.payload_object.perform }.should raise_error(Delayed::DeserializationError)
120
+ end
121
+
122
+ it "should try to load the class when it is unknown at the time of the deserialization" do
123
+ job = Delayed::Job.new
124
+ job['handler'] = "--- !ruby/object:JobThatDoesNotExist {}"
125
+
126
+ job.should_receive(:attempt_to_load).with('JobThatDoesNotExist').and_return(true)
127
+
128
+ lambda { job.payload_object.perform }.should raise_error(Delayed::DeserializationError)
129
+ end
130
+
131
+ it "should try include the namespace when loading unknown objects" do
132
+ job = Delayed::Job.new
133
+ job['handler'] = "--- !ruby/object:Delayed::JobThatDoesNotExist {}"
134
+ job.should_receive(:attempt_to_load).with('Delayed::JobThatDoesNotExist').and_return(true)
135
+ lambda { job.payload_object.perform }.should raise_error(Delayed::DeserializationError)
136
+ end
137
+
138
+ it "should also try to load structs when they are unknown (raises TypeError)" do
139
+ job = Delayed::Job.new
140
+ job['handler'] = "--- !ruby/struct:JobThatDoesNotExist {}"
141
+
142
+ job.should_receive(:attempt_to_load).with('JobThatDoesNotExist').and_return(true)
143
+
144
+ lambda { job.payload_object.perform }.should raise_error(Delayed::DeserializationError)
145
+ end
146
+
147
+ it "should try include the namespace when loading unknown structs" do
148
+ job = Delayed::Job.new
149
+ job['handler'] = "--- !ruby/struct:Delayed::JobThatDoesNotExist {}"
150
+
151
+ job.should_receive(:attempt_to_load).with('Delayed::JobThatDoesNotExist').and_return(true)
152
+ lambda { job.payload_object.perform }.should raise_error(Delayed::DeserializationError)
153
+ end
154
+
155
+ context "reschedule" do
156
+ before do
157
+ @job = Delayed::Job.create :payload_object => SimpleJob.new
158
+ end
159
+
160
+ context "and we want to destroy jobs" do
161
+ before do
162
+ Delayed::Job.destroy_failed_jobs = true
163
+ end
164
+
165
+ it "should be destroyed if it failed more than MAX_ATTEMPTS times" do
166
+ @job.should_receive(:destroy)
167
+ Delayed::Job::MAX_ATTEMPTS.times { @job.reschedule 'FAIL' }
168
+ end
169
+
170
+ it "should not be destroyed if failed fewer than MAX_ATTEMPTS times" do
171
+ @job.should_not_receive(:destroy)
172
+ (Delayed::Job::MAX_ATTEMPTS - 1).times { @job.reschedule 'FAIL' }
173
+ end
174
+ end
175
+
176
+ context "and we don't want to destroy jobs" do
177
+ before do
178
+ Delayed::Job.destroy_failed_jobs = false
179
+ end
180
+
181
+ it "should be failed if it failed more than MAX_ATTEMPTS times" do
182
+ @job.reload.failed_at.should == nil
183
+ Delayed::Job::MAX_ATTEMPTS.times { @job.reschedule 'FAIL' }
184
+ @job.reload.failed_at.should_not == nil
185
+ end
186
+
187
+ it "should not be failed if it failed fewer than MAX_ATTEMPTS times" do
188
+ (Delayed::Job::MAX_ATTEMPTS - 1).times { @job.reschedule 'FAIL' }
189
+ @job.reload.failed_at.should == nil
190
+ end
191
+
192
+ end
193
+ end
194
+
195
+ it "should fail after MAX_RUN_TIME" do
196
+ @job = Delayed::Job.create :payload_object => LongRunningJob.new
197
+ Delayed::Job.reserve_and_run_one_job(1.second)
198
+ @job.reload.last_error.should =~ /expired/
199
+ @job.attempts.should == 1
200
+ end
201
+
202
+ it "should never find failed jobs" do
203
+ @job = Delayed::Job.create :payload_object => SimpleJob.new, :attempts => 50, :failed_at => Time.now
204
+ Delayed::Job.find_available(1).length.should == 0
205
+ end
206
+
207
+ context "when another worker is already performing an task, it" do
208
+
209
+ before :each do
210
+ Delayed::Job.worker_name = 'worker1'
211
+ @job = Delayed::Job.create :payload_object => SimpleJob.new, :locked_by => 'worker1', :locked_at => Delayed::Job.db_time_now - 5.minutes
212
+ end
213
+
214
+ it "should not allow a second worker to get exclusive access" do
215
+ @job.lock_exclusively!(4.hours, 'worker2').should == false
216
+ end
217
+
218
+ it "should allow a second worker to get exclusive access if the timeout has passed" do
219
+ @job.lock_exclusively!(1.minute, 'worker2').should == true
220
+ end
221
+
222
+ it "should be able to get access to the task if it was started more then max_age ago" do
223
+ @job.locked_at = 5.hours.ago
224
+ @job.save
225
+
226
+ @job.lock_exclusively! 4.hours, 'worker2'
227
+ @job.reload
228
+ @job.locked_by.should == 'worker2'
229
+ @job.locked_at.should > 1.minute.ago
230
+ end
231
+
232
+ it "should not be found by another worker" do
233
+ Delayed::Job.worker_name = 'worker2'
234
+
235
+ Delayed::Job.find_available(1, 6.minutes).length.should == 0
236
+ end
237
+
238
+ it "should be found by another worker if the time has expired" do
239
+ Delayed::Job.worker_name = 'worker2'
240
+
241
+ Delayed::Job.find_available(1, 4.minutes).length.should == 1
242
+ end
243
+
244
+ it "should be able to get exclusive access again when the worker name is the same" do
245
+ @job.lock_exclusively! 5.minutes, 'worker1'
246
+ @job.lock_exclusively! 5.minutes, 'worker1'
247
+ @job.lock_exclusively! 5.minutes, 'worker1'
248
+ end
249
+ end
250
+
251
+ context "when another worker has worked on a task since the job was found to be available, it" do
252
+
253
+ before :each do
254
+ Delayed::Job.worker_name = 'worker1'
255
+ @job = Delayed::Job.create :payload_object => SimpleJob.new
256
+ @job_copy_for_worker_2 = Delayed::Job.find(@job.id)
257
+ end
258
+
259
+ it "should not allow a second worker to get exclusive access if already successfully processed by worker1" do
260
+ @job.delete
261
+ @job_copy_for_worker_2.lock_exclusively!(4.hours, 'worker2').should == false
262
+ end
263
+
264
+ 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
265
+ @job.update_attributes(:attempts => 1, :run_at => Time.now + 1.day)
266
+ @job_copy_for_worker_2.lock_exclusively!(4.hours, 'worker2').should == false
267
+ end
268
+ end
269
+
270
+ context "#name" do
271
+ it "should be the class name of the job that was enqueued" do
272
+ Delayed::Job.create(:payload_object => ErrorJob.new ).name.should == 'ErrorJob'
273
+ end
274
+
275
+ it "should be the method that will be called if its a performable method object" do
276
+ Delayed::Job.send_later(:clear_locks!)
277
+ Delayed::Job.last.name.should == 'Delayed::Job.clear_locks!'
278
+
279
+ end
280
+ it "should be the instance method that will be called if its a performable method object" do
281
+ story = Story.create :text => "..."
282
+
283
+ story.send_later(:save)
284
+
285
+ Delayed::Job.last.name.should == 'Story#save'
286
+ end
287
+ end
288
+
289
+ context "worker prioritization" do
290
+
291
+ before(:each) do
292
+ Delayed::Job.max_priority = nil
293
+ Delayed::Job.min_priority = nil
294
+ end
295
+
296
+ it "should only work_off jobs that are >= min_priority" do
297
+ Delayed::Job.min_priority = -5
298
+ Delayed::Job.max_priority = 5
299
+ SimpleJob.runs.should == 0
300
+
301
+ Delayed::Job.enqueue SimpleJob.new, -10
302
+ Delayed::Job.enqueue SimpleJob.new, 0
303
+ Delayed::Job.work_off
304
+
305
+ SimpleJob.runs.should == 1
306
+ end
307
+
308
+ it "should only work_off jobs that are <= max_priority" do
309
+ Delayed::Job.min_priority = -5
310
+ Delayed::Job.max_priority = 5
311
+ SimpleJob.runs.should == 0
312
+
313
+ Delayed::Job.enqueue SimpleJob.new, 10
314
+ Delayed::Job.enqueue SimpleJob.new, 0
315
+
316
+ Delayed::Job.work_off
317
+
318
+ SimpleJob.runs.should == 1
319
+ end
320
+
321
+ it "should fetch jobs ordered by priority" do
322
+ number_of_jobs = 10
323
+ number_of_jobs.times { Delayed::Job.enqueue SimpleJob.new, rand(10) }
324
+ jobs = Delayed::Job.find_available(10)
325
+ ordered = true
326
+ jobs[1..-1].each_index{ |i|
327
+ if (jobs[i].priority < jobs[i+1].priority)
328
+ ordered = false
329
+ break
330
+ end
331
+ }
332
+ ordered.should == true
333
+ end
334
+
335
+ end
336
+
337
+ context "when pulling jobs off the queue for processing, it" do
338
+ before(:each) do
339
+ @job = Delayed::Job.create(
340
+ :payload_object => SimpleJob.new,
341
+ :locked_by => 'worker1',
342
+ :locked_at => Delayed::Job.db_time_now - 5.minutes)
343
+ end
344
+
345
+ it "should leave the queue in a consistent state and not run the job if locking fails" do
346
+ SimpleJob.runs.should == 0
347
+ @job.stub!(:lock_exclusively!).with(any_args).once.and_return(false)
348
+ Delayed::Job.should_receive(:find_available).once.and_return([@job])
349
+ Delayed::Job.work_off(1)
350
+ SimpleJob.runs.should == 0
351
+ end
352
+
353
+ end
354
+
355
+ context "while running alongside other workers that locked jobs, it" do
356
+ before(:each) do
357
+ Delayed::Job.worker_name = 'worker1'
358
+ Delayed::Job.create(:payload_object => SimpleJob.new, :locked_by => 'worker1', :locked_at => (Delayed::Job.db_time_now - 1.minutes))
359
+ Delayed::Job.create(:payload_object => SimpleJob.new, :locked_by => 'worker2', :locked_at => (Delayed::Job.db_time_now - 1.minutes))
360
+ Delayed::Job.create(:payload_object => SimpleJob.new)
361
+ Delayed::Job.create(:payload_object => SimpleJob.new, :locked_by => 'worker1', :locked_at => (Delayed::Job.db_time_now - 1.minutes))
362
+ end
363
+
364
+ it "should ingore locked jobs from other workers" do
365
+ Delayed::Job.worker_name = 'worker3'
366
+ SimpleJob.runs.should == 0
367
+ Delayed::Job.work_off
368
+ SimpleJob.runs.should == 1 # runs the one open job
369
+ end
370
+
371
+ it "should find our own jobs regardless of locks" do
372
+ Delayed::Job.worker_name = 'worker1'
373
+ SimpleJob.runs.should == 0
374
+ Delayed::Job.work_off
375
+ SimpleJob.runs.should == 3 # runs open job plus worker1 jobs that were already locked
376
+ end
377
+ end
378
+
379
+ context "while running with locked and expired jobs, it" do
380
+ before(:each) do
381
+ Delayed::Job.worker_name = 'worker1'
382
+ exp_time = Delayed::Job.db_time_now - (1.minutes + Delayed::Job::MAX_RUN_TIME)
383
+ Delayed::Job.create(:payload_object => SimpleJob.new, :locked_by => 'worker1', :locked_at => exp_time)
384
+ Delayed::Job.create(:payload_object => SimpleJob.new, :locked_by => 'worker2', :locked_at => (Delayed::Job.db_time_now - 1.minutes))
385
+ Delayed::Job.create(:payload_object => SimpleJob.new)
386
+ Delayed::Job.create(:payload_object => SimpleJob.new, :locked_by => 'worker1', :locked_at => (Delayed::Job.db_time_now - 1.minutes))
387
+ end
388
+
389
+ it "should only find unlocked and expired jobs" do
390
+ Delayed::Job.worker_name = 'worker3'
391
+ SimpleJob.runs.should == 0
392
+ Delayed::Job.work_off
393
+ SimpleJob.runs.should == 2 # runs the one open job and one expired job
394
+ end
395
+
396
+ it "should ignore locks when finding our own jobs" do
397
+ Delayed::Job.worker_name = 'worker1'
398
+ SimpleJob.runs.should == 0
399
+ Delayed::Job.work_off
400
+ SimpleJob.runs.should == 3 # runs open job plus worker1 jobs
401
+ # This is useful in the case of a crash/restart on worker1, but make sure multiple workers on the same host have unique names!
402
+ end
403
+
404
+ end
405
+
406
+ end
@@ -0,0 +1,17 @@
1
+ require File.dirname(__FILE__) + '/database'
2
+
3
+ describe "A story" do
4
+
5
+ before(:all) do
6
+ @story = Story.create :text => "Once upon a time..."
7
+ end
8
+
9
+ it "should be shared" do
10
+ @story.tell.should == 'Once upon a time...'
11
+ end
12
+
13
+ it "should not return its result if it storytelling is delayed" do
14
+ @story.send_later(:tell).should_not == 'Once upon a time...'
15
+ end
16
+
17
+ end
@@ -0,0 +1 @@
1
+ require File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib', 'delayed', 'tasks'))
metadata ADDED
@@ -0,0 +1,93 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: mathie-delayed_job
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 1
7
+ - 8
8
+ - 4
9
+ version: 1.8.4
10
+ platform: ruby
11
+ authors:
12
+ - Brandon Keepers
13
+ - "Tobias L\xC3\xBCtke"
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2010-03-03 00:00:00 +00:00
19
+ default_executable:
20
+ dependencies: []
21
+
22
+ description: Delayed_job (or DJ) encapsulates the common pattern of asynchronously executing longer tasks in the background. It is a direct extraction from Shopify where the job table is responsible for a multitude of core tasks.
23
+ email: tobi@leetsoft.com
24
+ executables: []
25
+
26
+ extensions: []
27
+
28
+ extra_rdoc_files:
29
+ - README.textile
30
+ files:
31
+ - .gitignore
32
+ - MIT-LICENSE
33
+ - README.textile
34
+ - Rakefile
35
+ - VERSION
36
+ - contrib/delayed_job.monitrc
37
+ - generators/delayed_job/delayed_job_generator.rb
38
+ - generators/delayed_job/templates/migration.rb
39
+ - generators/delayed_job/templates/script
40
+ - init.rb
41
+ - lib/delayed/command.rb
42
+ - lib/delayed/job.rb
43
+ - lib/delayed/message_sending.rb
44
+ - lib/delayed/performable_method.rb
45
+ - lib/delayed/recipes.rb
46
+ - lib/delayed/tasks.rb
47
+ - lib/delayed/worker.rb
48
+ - lib/delayed_job.rb
49
+ - mathie-delayed_job.gemspec
50
+ - recipes/delayed_job.rb
51
+ - spec/database.rb
52
+ - spec/delayed_method_spec.rb
53
+ - spec/job_spec.rb
54
+ - spec/story_spec.rb
55
+ - tasks/jobs.rake
56
+ has_rdoc: true
57
+ homepage: http://github.com/mathie/delayed_job
58
+ licenses: []
59
+
60
+ post_install_message:
61
+ rdoc_options:
62
+ - --main
63
+ - README.textile
64
+ - --inline-source
65
+ - --line-numbers
66
+ require_paths:
67
+ - lib
68
+ required_ruby_version: !ruby/object:Gem::Requirement
69
+ requirements:
70
+ - - ">="
71
+ - !ruby/object:Gem::Version
72
+ segments:
73
+ - 0
74
+ version: "0"
75
+ required_rubygems_version: !ruby/object:Gem::Requirement
76
+ requirements:
77
+ - - ">="
78
+ - !ruby/object:Gem::Version
79
+ segments:
80
+ - 0
81
+ version: "0"
82
+ requirements: []
83
+
84
+ rubyforge_project:
85
+ rubygems_version: 1.3.6
86
+ signing_key:
87
+ specification_version: 3
88
+ summary: Database-backed asynchronous priority queue system -- Extracted from Shopify
89
+ test_files:
90
+ - spec/database.rb
91
+ - spec/delayed_method_spec.rb
92
+ - spec/job_spec.rb
93
+ - spec/story_spec.rb