delayed-job-ajaycb 2.0.10

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.
Files changed (39) hide show
  1. data/MIT-LICENSE +20 -0
  2. data/README.textile +250 -0
  3. data/contrib/delayed_job.monitrc +14 -0
  4. data/contrib/delayed_job_multiple.monitrc +23 -0
  5. data/generators/delayed_job/delayed_job_generator.rb +28 -0
  6. data/generators/delayed_job/templates/migration.rb +22 -0
  7. data/generators/delayed_job/templates/migration_queue_name.rb +12 -0
  8. data/generators/delayed_job/templates/script +5 -0
  9. data/lib/delayed/backend/active_record.rb +97 -0
  10. data/lib/delayed/backend/base.rb +126 -0
  11. data/lib/delayed/backend/data_mapper.rb +125 -0
  12. data/lib/delayed/backend/mongo_mapper.rb +110 -0
  13. data/lib/delayed/command.rb +116 -0
  14. data/lib/delayed/deserialization_error.rb +4 -0
  15. data/lib/delayed/message_sending.rb +53 -0
  16. data/lib/delayed/performable_method.rb +62 -0
  17. data/lib/delayed/railtie.rb +10 -0
  18. data/lib/delayed/recipes.rb +50 -0
  19. data/lib/delayed/tasks.rb +15 -0
  20. data/lib/delayed/worker.rb +190 -0
  21. data/lib/delayed_job.rb +15 -0
  22. data/rails/init.rb +5 -0
  23. data/recipes/delayed_job.rb +1 -0
  24. data/spec/backend/active_record_job_spec.rb +70 -0
  25. data/spec/backend/data_mapper_job_spec.rb +16 -0
  26. data/spec/backend/mongo_mapper_job_spec.rb +94 -0
  27. data/spec/backend/shared_backend_spec.rb +342 -0
  28. data/spec/delayed_method_spec.rb +46 -0
  29. data/spec/message_sending_spec.rb +89 -0
  30. data/spec/performable_method_spec.rb +53 -0
  31. data/spec/sample_jobs.rb +26 -0
  32. data/spec/setup/active_record.rb +34 -0
  33. data/spec/setup/data_mapper.rb +8 -0
  34. data/spec/setup/mongo_mapper.rb +17 -0
  35. data/spec/spec_helper.rb +28 -0
  36. data/spec/story_spec.rb +17 -0
  37. data/spec/worker_spec.rb +237 -0
  38. data/tasks/jobs.rake +1 -0
  39. metadata +329 -0
@@ -0,0 +1,16 @@
1
+ require 'spec_helper'
2
+ require 'backend/shared_backend_spec'
3
+ require 'delayed/backend/data_mapper'
4
+
5
+ describe Delayed::Backend::DataMapper::Job do
6
+ before(:all) do
7
+ @backend = Delayed::Backend::DataMapper::Job
8
+ end
9
+
10
+ before(:each) do
11
+ # reset database before each example is run
12
+ DataMapper.auto_migrate!
13
+ end
14
+
15
+ it_should_behave_like 'a backend'
16
+ end
@@ -0,0 +1,94 @@
1
+ require 'spec_helper'
2
+ require 'backend/shared_backend_spec'
3
+ require 'delayed/backend/mongo_mapper'
4
+
5
+ describe Delayed::Backend::MongoMapper::Job do
6
+ before(:all) do
7
+ @backend = Delayed::Backend::MongoMapper::Job
8
+ end
9
+
10
+ before(:each) do
11
+ MongoMapper.database.collections.each(&:remove)
12
+ end
13
+
14
+ it_should_behave_like 'a backend'
15
+
16
+ describe "indexes" do
17
+ it "should have combo index on priority and run_at" do
18
+ @backend.collection.index_information.detect { |index| index[0] == 'priority_1_run_at_1' }.should_not be_nil
19
+ end
20
+
21
+ it "should have index on locked_by" do
22
+ @backend.collection.index_information.detect { |index| index[0] == 'locked_by_1' }.should_not be_nil
23
+ end
24
+ end
25
+
26
+ describe "delayed method" do
27
+ class MongoStoryReader
28
+ def read(story)
29
+ "Epilog: #{story.tell}"
30
+ end
31
+ end
32
+
33
+ class MongoStory
34
+ include ::MongoMapper::Document
35
+ key :text, String
36
+
37
+ def tell
38
+ text
39
+ end
40
+ end
41
+
42
+ it "should ignore not found errors because they are permanent" do
43
+ story = MongoStory.create :text => 'Once upon a time...'
44
+ job = story.delay.tell
45
+ story.destroy
46
+ lambda { job.invoke_job }.should_not raise_error
47
+ end
48
+
49
+ it "should store the object as string" do
50
+ story = MongoStory.create :text => 'Once upon a time...'
51
+ job = story.delay.tell
52
+
53
+ job.payload_object.class.should == Delayed::PerformableMethod
54
+ job.payload_object.object.should == story
55
+ job.payload_object.method.should == :tell
56
+ job.payload_object.args.should == []
57
+ job.payload_object.perform.should == 'Once upon a time...'
58
+ end
59
+
60
+ it "should store arguments as string" do
61
+ story = MongoStory.create :text => 'Once upon a time...'
62
+ job = MongoStoryReader.new.delay.read(story)
63
+ job.payload_object.class.should == Delayed::PerformableMethod
64
+ job.payload_object.method.should == :read
65
+ job.payload_object.args.should == [story]
66
+ job.payload_object.perform.should == 'Epilog: Once upon a time...'
67
+ end
68
+ end
69
+
70
+ describe "before_fork" do
71
+ after do
72
+ MongoMapper.connection.connect
73
+ end
74
+
75
+ it "should disconnect" do
76
+ lambda do
77
+ Delayed::Backend::MongoMapper::Job.before_fork
78
+ end.should change { !!MongoMapper.connection.connected? }.from(true).to(false)
79
+ end
80
+ end
81
+
82
+ describe "after_fork" do
83
+ before do
84
+ MongoMapper.connection.close
85
+ end
86
+
87
+ it "should call reconnect" do
88
+ lambda do
89
+ Delayed::Backend::MongoMapper::Job.after_fork
90
+ end.should change { !!MongoMapper.connection.connected? }.from(false).to(true)
91
+ end
92
+ end
93
+
94
+ end
@@ -0,0 +1,342 @@
1
+ shared_examples_for 'a backend' do
2
+ def create_job(opts = {})
3
+ @backend.create(opts.merge(:payload_object => SimpleJob.new))
4
+ end
5
+
6
+ before do
7
+ Delayed::Worker.max_priority = nil
8
+ Delayed::Worker.min_priority = nil
9
+ Delayed::Worker.default_priority = 99
10
+ SimpleJob.runs = 0
11
+ end
12
+
13
+ it "should set run_at automatically if not set" do
14
+ @backend.create(:payload_object => ErrorJob.new ).run_at.should_not be_nil
15
+ end
16
+
17
+ it "should not set run_at automatically if already set" do
18
+ later = @backend.db_time_now + 5.minutes
19
+ @backend.create(:payload_object => ErrorJob.new, :run_at => later).run_at.should be_close(later, 1)
20
+ end
21
+
22
+ it "should raise ArgumentError when handler doesn't respond_to :perform" do
23
+ lambda { @backend.enqueue(Object.new) }.should raise_error(ArgumentError)
24
+ end
25
+
26
+ it "should increase count after enqueuing items" do
27
+ @backend.enqueue SimpleJob.new
28
+ @backend.count.should == 1
29
+ end
30
+
31
+ it "should be able to set priority when enqueuing items" do
32
+ @job = @backend.enqueue SimpleJob.new, 5
33
+ @job.priority.should == 5
34
+ end
35
+
36
+ it "should use default priority when it is not set" do
37
+ @job = @backend.enqueue SimpleJob.new
38
+ @job.priority.should == 99
39
+ end
40
+
41
+ it "should be able to set run_at when enqueuing items" do
42
+ later = @backend.db_time_now + 5.minutes
43
+ @job = @backend.enqueue SimpleJob.new, 5, later
44
+ @job.run_at.should be_close(later, 1)
45
+ end
46
+
47
+ it "should work with jobs in modules" do
48
+ M::ModuleJob.runs = 0
49
+ job = @backend.enqueue M::ModuleJob.new
50
+ lambda { job.invoke_job }.should change { M::ModuleJob.runs }.from(0).to(1)
51
+ end
52
+
53
+ it "should raise an DeserializationError when the job class is totally unknown" do
54
+ job = @backend.new :handler => "--- !ruby/object:JobThatDoesNotExist {}"
55
+ lambda { job.payload_object.perform }.should raise_error(Delayed::DeserializationError)
56
+ end
57
+
58
+ it "should raise an DeserializationError when the job is badly encoded" do
59
+ job = @backend.new :handler => "--- !ruby/object:SimpleJob {"
60
+ lambda { job.payload_object.perform }.should raise_error(Delayed::DeserializationError)
61
+ end
62
+
63
+ it "should try to load the class when it is unknown at the time of the deserialization" do
64
+ job = @backend.new :handler => "--- !ruby/object:JobThatDoesNotExist {}"
65
+ job.should_receive(:attempt_to_load).with('JobThatDoesNotExist').and_return(true)
66
+ lambda { job.payload_object.perform }.should raise_error(Delayed::DeserializationError)
67
+ end
68
+
69
+ it "should try include the namespace when loading unknown objects" do
70
+ job = @backend.new :handler => "--- !ruby/object:Delayed::JobThatDoesNotExist {}"
71
+ job.should_receive(:attempt_to_load).with('Delayed::JobThatDoesNotExist').and_return(true)
72
+ lambda { job.payload_object.perform }.should raise_error(Delayed::DeserializationError)
73
+ end
74
+
75
+ it "should also try to load structs when they are unknown (raises TypeError)" do
76
+ job = @backend.new :handler => "--- !ruby/struct:JobThatDoesNotExist {}"
77
+ job.should_receive(:attempt_to_load).with('JobThatDoesNotExist').and_return(true)
78
+ lambda { job.payload_object.perform }.should raise_error(Delayed::DeserializationError)
79
+ end
80
+
81
+ it "should try include the namespace when loading unknown structs" do
82
+ job = @backend.new :handler => "--- !ruby/struct:Delayed::JobThatDoesNotExist {}"
83
+ job.should_receive(:attempt_to_load).with('Delayed::JobThatDoesNotExist').and_return(true)
84
+ lambda { job.payload_object.perform }.should raise_error(Delayed::DeserializationError)
85
+ end
86
+
87
+ describe "find_available" do
88
+ it "should not find failed jobs" do
89
+ @job = create_job :attempts => 50, :failed_at => @backend.db_time_now
90
+ @backend.find_available('worker', 5, 1.second).should_not include(@job)
91
+ end
92
+
93
+ it "should not find jobs scheduled for the future" do
94
+ @job = create_job :run_at => (@backend.db_time_now + 1.minute)
95
+ @backend.find_available('worker', 5, 4.hours).should_not include(@job)
96
+ end
97
+
98
+ it "should not find jobs locked by another worker" do
99
+ @job = create_job(:locked_by => 'other_worker', :locked_at => @backend.db_time_now - 1.minute)
100
+ @backend.find_available('worker', 5, 4.hours).should_not include(@job)
101
+ end
102
+
103
+ it "should find open jobs" do
104
+ @job = create_job
105
+ @backend.find_available('worker', 5, 4.hours).should include(@job)
106
+ end
107
+
108
+ it "should find expired jobs" do
109
+ @job = create_job(:locked_by => 'worker', :locked_at => @backend.db_time_now - 2.minutes)
110
+ @backend.find_available('worker', 5, 1.minute).should include(@job)
111
+ end
112
+
113
+ it "should find own jobs" do
114
+ @job = create_job(:locked_by => 'worker', :locked_at => (@backend.db_time_now - 1.minutes))
115
+ @backend.find_available('worker', 5, 4.hours).should include(@job)
116
+ end
117
+
118
+ it "should find only the right amount of jobs" do
119
+ 10.times { create_job }
120
+ @backend.find_available('worker', 7, 4.hours).should have(7).jobs
121
+ end
122
+ end
123
+
124
+ context "when another worker is already performing an task, it" do
125
+
126
+ before :each do
127
+ @job = @backend.create :payload_object => SimpleJob.new, :locked_by => 'worker1', :locked_at => @backend.db_time_now - 5.minutes
128
+ end
129
+
130
+ it "should not allow a second worker to get exclusive access" do
131
+ @job.lock_exclusively!(4.hours, 'worker2').should == false
132
+ end
133
+
134
+ it "should allow a second worker to get exclusive access if the timeout has passed" do
135
+ @job.lock_exclusively!(1.minute, 'worker2').should == true
136
+ end
137
+
138
+ it "should be able to get access to the task if it was started more then max_age ago" do
139
+ @job.locked_at = 5.hours.ago
140
+ @job.save
141
+
142
+ @job.lock_exclusively! 4.hours, 'worker2'
143
+ @job.reload
144
+ @job.locked_by.should == 'worker2'
145
+ @job.locked_at.should > 1.minute.ago
146
+ end
147
+
148
+ it "should not be found by another worker" do
149
+ @backend.find_available('worker2', 1, 6.minutes).length.should == 0
150
+ end
151
+
152
+ it "should be found by another worker if the time has expired" do
153
+ @backend.find_available('worker2', 1, 4.minutes).length.should == 1
154
+ end
155
+
156
+ it "should be able to get exclusive access again when the worker name is the same" do
157
+ @job.lock_exclusively!(5.minutes, 'worker1').should be_true
158
+ @job.lock_exclusively!(5.minutes, 'worker1').should be_true
159
+ @job.lock_exclusively!(5.minutes, 'worker1').should be_true
160
+ end
161
+ end
162
+
163
+ context "when another worker has worked on a task since the job was found to be available, it" do
164
+
165
+ before :each do
166
+ @job = @backend.create :payload_object => SimpleJob.new
167
+ @job_copy_for_worker_2 = @backend.find(@job.id)
168
+ end
169
+
170
+ it "should not allow a second worker to get exclusive access if already successfully processed by worker1" do
171
+ @job.destroy
172
+ @job_copy_for_worker_2.lock_exclusively!(4.hours, 'worker2').should == false
173
+ end
174
+
175
+ 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
176
+ @job.update_attributes(:attempts => 1, :run_at => 1.day.from_now)
177
+ @job_copy_for_worker_2.lock_exclusively!(4.hours, 'worker2').should == false
178
+ end
179
+ end
180
+
181
+
182
+ describe "reserve" do
183
+ before do
184
+ Delayed::Worker.max_run_time = 2.minutes
185
+ @worker = Delayed::Worker.new(:quiet => true)
186
+ end
187
+
188
+ it "should not reserve failed jobs" do
189
+ create_job :attempts => 50, :failed_at => described_class.db_time_now
190
+ described_class.reserve(@worker).should be_nil
191
+ end
192
+
193
+ it "should not reserve jobs scheduled for the future" do
194
+ create_job :run_at => (described_class.db_time_now + 1.minute)
195
+ described_class.reserve(@worker).should be_nil
196
+ end
197
+
198
+ it "should lock the job so other workers can't reserve it" do
199
+ job = create_job
200
+ described_class.reserve(@worker).should == job
201
+ new_worker = Delayed::Worker.new(:quiet => true)
202
+ new_worker.name = 'worker2'
203
+ described_class.reserve(new_worker).should be_nil
204
+ end
205
+
206
+ it "should reserve open jobs" do
207
+ job = create_job
208
+ described_class.reserve(@worker).should == job
209
+ end
210
+
211
+ it "should reserve expired jobs" do
212
+ job = create_job(:locked_by => @worker.name, :locked_at => described_class.db_time_now - 3.minutes)
213
+ described_class.reserve(@worker).should == job
214
+ end
215
+
216
+ it "should reserve own jobs" do
217
+ job = create_job(:locked_by => @worker.name, :locked_at => (described_class.db_time_now - 1.minutes))
218
+ described_class.reserve(@worker).should == job
219
+ end
220
+ end
221
+
222
+ context "#name" do
223
+ it "should be the class name of the job that was enqueued" do
224
+ @backend.create(:payload_object => ErrorJob.new ).name.should == 'ErrorJob'
225
+ end
226
+
227
+ it "should be the method that will be called if its a performable method object" do
228
+ @job = Story.delay.create
229
+ @job.name.should == "Story.create"
230
+ end
231
+
232
+ it "should be the instance method that will be called if its a performable method object" do
233
+ @job = Story.create(:text => "...").delay.save
234
+ @job.name.should == 'Story#save'
235
+ end
236
+ end
237
+
238
+ context "worker prioritization" do
239
+ before(:each) do
240
+ Delayed::Worker.max_priority = nil
241
+ Delayed::Worker.min_priority = nil
242
+ end
243
+
244
+ it "should fetch jobs ordered by priority" do
245
+ 10.times { @backend.enqueue SimpleJob.new, rand(10) }
246
+ jobs = @backend.find_available('worker', 10)
247
+ jobs.size.should == 10
248
+ jobs.each_cons(2) do |a, b|
249
+ a.priority.should <= b.priority
250
+ end
251
+ end
252
+
253
+ it "should only find jobs greater than or equal to min priority" do
254
+ min = 5
255
+ Delayed::Worker.min_priority = min
256
+ 10.times {|i| @backend.enqueue SimpleJob.new, i }
257
+ jobs = @backend.find_available('worker', 10)
258
+ jobs.each {|job| job.priority.should >= min}
259
+ end
260
+
261
+ it "should only find jobs less than or equal to max priority" do
262
+ max = 5
263
+ Delayed::Worker.max_priority = max
264
+ 10.times {|i| @backend.enqueue SimpleJob.new, i }
265
+ jobs = @backend.find_available('worker', 10)
266
+ jobs.each {|job| job.priority.should <= max}
267
+ end
268
+ end
269
+
270
+ context "clear_locks!" do
271
+ before do
272
+ @job = create_job(:locked_by => 'worker', :locked_at => @backend.db_time_now)
273
+ end
274
+
275
+ it "should clear locks for the given worker" do
276
+ @backend.clear_locks!('worker')
277
+ @backend.find_available('worker2', 5, 1.minute).should include(@job)
278
+ end
279
+
280
+ it "should not clear locks for other workers" do
281
+ @backend.clear_locks!('worker1')
282
+ @backend.find_available('worker1', 5, 1.minute).should_not include(@job)
283
+ end
284
+ end
285
+
286
+ context "unlock" do
287
+ before do
288
+ @job = create_job(:locked_by => 'worker', :locked_at => @backend.db_time_now)
289
+ end
290
+
291
+ it "should clear locks" do
292
+ @job.unlock
293
+ @job.locked_by.should be_nil
294
+ @job.locked_at.should be_nil
295
+ end
296
+ end
297
+
298
+ context "large handler" do
299
+ before do
300
+ text = "Lorem ipsum dolor sit amet. " * 1000
301
+ @job = @backend.enqueue Delayed::PerformableMethod.new(text, :length, {})
302
+ end
303
+
304
+ it "should have an id" do
305
+ @job.id.should_not be_nil
306
+ end
307
+ end
308
+
309
+ context "max_attempts" do
310
+ before(:each) do
311
+ @job = described_class.enqueue SimpleJob.new
312
+ end
313
+
314
+ it 'should not be defined' do
315
+ @job.max_attempts.should be_nil
316
+ end
317
+
318
+ it 'should use the max_retries value on the payload when defined' do
319
+ @job.payload_object.stub!(:max_attempts).and_return(99)
320
+ @job.max_attempts.should == 99
321
+ end
322
+ end
323
+
324
+ describe "worker integration" do
325
+ before do
326
+ @worker = Delayed::Worker.new(:max_priority => nil, :min_priority => nil, :quiet => true)
327
+ end
328
+
329
+ describe "running a job" do
330
+
331
+ context "when the job raises a deserialization error" do
332
+ it "should mark the job as failed" do
333
+ Delayed::Worker.destroy_failed_jobs = false
334
+ job = described_class.create! :handler => "--- !ruby/object:JobThatDoesNotExist {}"
335
+ @worker.work_off
336
+ job.reload
337
+ job.failed_at.should_not be_nil
338
+ end
339
+ end
340
+ end
341
+ end
342
+ end