mathie-delayed_job 1.8.4

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,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