merged cont.
[opensuse:yast-rest-service.git] / webyast / vendor / plugins / delayed_job / spec / job_spec.rb
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 module M
14   class ModuleJob
15     cattr_accessor :runs; self.runs = 0
16     def perform; @@runs += 1; end    
17   end
18   
19 end
20
21 describe Delayed::Job do
22   before  do               
23     Delayed::Job.max_priority = nil
24     Delayed::Job.min_priority = nil      
25     
26     Delayed::Job.delete_all
27   end
28   
29   before(:each) do
30     SimpleJob.runs = 0
31   end
32
33   it "should set run_at automatically if not set" do
34     Delayed::Job.create(:payload_object => ErrorJob.new ).run_at.should_not == nil
35   end
36
37   it "should not set run_at automatically if already set" do
38     later = 5.minutes.from_now
39     Delayed::Job.create(:payload_object => ErrorJob.new, :run_at => later).run_at.should == later
40   end
41
42   it "should raise ArgumentError when handler doesn't respond_to :perform" do
43     lambda { Delayed::Job.enqueue(Object.new) }.should raise_error(ArgumentError)
44   end
45
46   it "should increase count after enqueuing items" do
47     Delayed::Job.enqueue SimpleJob.new
48     Delayed::Job.count.should == 1
49   end
50
51   it "should be able to set priority when enqueuing items" do
52     Delayed::Job.enqueue SimpleJob.new, 5
53     Delayed::Job.first.priority.should == 5
54   end
55
56   it "should be able to set run_at when enqueuing items" do
57     later = (Delayed::Job.db_time_now+5.minutes)
58     Delayed::Job.enqueue SimpleJob.new, 5, later
59
60     # use be close rather than equal to because millisecond values cn be lost in DB round trip
61     Delayed::Job.first.run_at.should be_close(later, 1)
62   end
63
64   it "should call perform on jobs when running work_off" do
65     SimpleJob.runs.should == 0
66
67     Delayed::Job.enqueue SimpleJob.new
68     Delayed::Job.work_off
69
70     SimpleJob.runs.should == 1
71   end
72                      
73                      
74   it "should work with eval jobs" do
75     $eval_job_ran = false
76
77     Delayed::Job.enqueue do <<-JOB
78       $eval_job_ran = true
79     JOB
80     end
81
82     Delayed::Job.work_off
83
84     $eval_job_ran.should == true
85   end
86                    
87   it "should work with jobs in modules" do
88     M::ModuleJob.runs.should == 0
89
90     Delayed::Job.enqueue M::ModuleJob.new
91     Delayed::Job.work_off
92
93     M::ModuleJob.runs.should == 1
94   end
95                    
96   it "should re-schedule by about 1 second at first and increment this more and more minutes when it fails to execute properly" do
97     Delayed::Job.enqueue ErrorJob.new
98     Delayed::Job.work_off(1)
99
100     job = Delayed::Job.find(:first)
101
102     job.last_error.should =~ /did not work/
103     job.last_error.should =~ /job_spec.rb:10:in `perform'/
104     job.attempts.should == 1
105
106     job.run_at.should > Delayed::Job.db_time_now - 10.minutes
107     job.run_at.should < Delayed::Job.db_time_now + 10.minutes
108   end
109
110   it "should raise an DeserializationError when the job class is totally unknown" do
111
112     job = Delayed::Job.new
113     job['handler'] = "--- !ruby/object:JobThatDoesNotExist {}"
114
115     lambda { job.payload_object.perform }.should raise_error(Delayed::DeserializationError)
116   end
117
118   it "should try to load the class when it is unknown at the time of the deserialization" do
119     job = Delayed::Job.new
120     job['handler'] = "--- !ruby/object:JobThatDoesNotExist {}"
121
122     job.should_receive(:attempt_to_load).with('JobThatDoesNotExist').and_return(true)
123
124     lambda { job.payload_object.perform }.should raise_error(Delayed::DeserializationError)
125   end
126
127   it "should try include the namespace when loading unknown objects" do
128     job = Delayed::Job.new
129     job['handler'] = "--- !ruby/object:Delayed::JobThatDoesNotExist {}"
130     job.should_receive(:attempt_to_load).with('Delayed::JobThatDoesNotExist').and_return(true)
131     lambda { job.payload_object.perform }.should raise_error(Delayed::DeserializationError)
132   end
133
134   it "should also try to load structs when they are unknown (raises TypeError)" do
135     job = Delayed::Job.new
136     job['handler'] = "--- !ruby/struct:JobThatDoesNotExist {}"
137
138     job.should_receive(:attempt_to_load).with('JobThatDoesNotExist').and_return(true)
139
140     lambda { job.payload_object.perform }.should raise_error(Delayed::DeserializationError)
141   end
142
143   it "should try include the namespace when loading unknown structs" do
144     job = Delayed::Job.new
145     job['handler'] = "--- !ruby/struct:Delayed::JobThatDoesNotExist {}"
146
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 be failed if it failed more than MAX_ATTEMPTS times and we don't want to destroy jobs" do
152     default = Delayed::Job.destroy_failed_jobs
153     Delayed::Job.destroy_failed_jobs = false
154
155     @job = Delayed::Job.create :payload_object => SimpleJob.new, :attempts => 50
156     @job.reload.failed_at.should == nil
157     @job.reschedule 'FAIL'
158     @job.reload.failed_at.should_not == nil
159
160     Delayed::Job.destroy_failed_jobs = default
161   end
162
163   it "should be destroyed if it failed more than MAX_ATTEMPTS times and we want to destroy jobs" do
164     default = Delayed::Job.destroy_failed_jobs
165     Delayed::Job.destroy_failed_jobs = true
166
167     @job = Delayed::Job.create :payload_object => SimpleJob.new, :attempts => 50
168     @job.should_receive(:destroy)
169     @job.reschedule 'FAIL'
170
171     Delayed::Job.destroy_failed_jobs = default
172   end
173
174   it "should never find failed jobs" do
175     @job = Delayed::Job.create :payload_object => SimpleJob.new, :attempts => 50, :failed_at => Delayed::Job.db_time_now
176     Delayed::Job.find_available(1).length.should == 0
177   end
178
179   context "when another worker is already performing an task, it" do
180
181     before :each do
182       Delayed::Job.worker_name = 'worker1'
183       @job = Delayed::Job.create :payload_object => SimpleJob.new, :locked_by => 'worker1', :locked_at => Delayed::Job.db_time_now - 5.minutes
184     end
185
186     it "should not allow a second worker to get exclusive access" do
187       @job.lock_exclusively!(4.hours, 'worker2').should == false
188     end
189
190     it "should allow a second worker to get exclusive access if the timeout has passed" do
191       @job.lock_exclusively!(1.minute, 'worker2').should == true
192     end      
193     
194     it "should be able to get access to the task if it was started more then max_age ago" do
195       @job.locked_at = 5.hours.ago
196       @job.save
197
198       @job.lock_exclusively! 4.hours, 'worker2'
199       @job.reload
200       @job.locked_by.should == 'worker2'
201       @job.locked_at.should > 1.minute.ago
202     end
203
204     it "should not be found by another worker" do
205       Delayed::Job.worker_name = 'worker2'
206
207       Delayed::Job.find_available(1, 6.minutes).length.should == 0
208     end
209
210     it "should be found by another worker if the time has expired" do
211       Delayed::Job.worker_name = 'worker2'
212
213       Delayed::Job.find_available(1, 4.minutes).length.should == 1
214     end
215
216     it "should be able to get exclusive access again when the worker name is the same" do
217       @job.lock_exclusively! 5.minutes, 'worker1'
218       @job.lock_exclusively! 5.minutes, 'worker1'
219       @job.lock_exclusively! 5.minutes, 'worker1'
220     end                                        
221   end            
222   
223   context "#name" do
224     it "should be the class name of the job that was enqueued" do
225       Delayed::Job.create(:payload_object => ErrorJob.new ).name.should == 'ErrorJob'
226     end
227
228     it "should be the method that will be called if its a performable method object" do
229       Delayed::Job.send_later(:clear_locks!)
230       Delayed::Job.last.name.should == 'Delayed::Job.clear_locks!'
231
232     end
233     it "should be the instance method that will be called if its a performable method object" do
234       story = Story.create :text => "..."                 
235       
236       story.send_later(:save)
237       
238       Delayed::Job.last.name.should == 'Story#save'
239     end
240   end
241   
242   context "worker prioritization" do
243     
244     before(:each) do
245       Delayed::Job.max_priority = nil
246       Delayed::Job.min_priority = nil      
247     end
248   
249     it "should only work_off jobs that are >= min_priority" do
250       Delayed::Job.min_priority = -5
251       Delayed::Job.max_priority = 5
252       SimpleJob.runs.should == 0
253     
254       Delayed::Job.enqueue SimpleJob.new, -10
255       Delayed::Job.enqueue SimpleJob.new, 0
256       Delayed::Job.work_off
257     
258       SimpleJob.runs.should == 1
259     end
260   
261     it "should only work_off jobs that are <= max_priority" do
262       Delayed::Job.min_priority = -5
263       Delayed::Job.max_priority = 5
264       SimpleJob.runs.should == 0
265     
266       Delayed::Job.enqueue SimpleJob.new, 10
267       Delayed::Job.enqueue SimpleJob.new, 0
268
269       Delayed::Job.work_off
270
271       SimpleJob.runs.should == 1
272     end                         
273    
274   end
275   
276   context "when pulling jobs off the queue for processing, it" do
277     before(:each) do
278       @job = Delayed::Job.create(
279         :payload_object => SimpleJob.new, 
280         :locked_by => 'worker1', 
281         :locked_at => Delayed::Job.db_time_now - 5.minutes)
282     end
283
284     it "should leave the queue in a consistent state and not run the job if locking fails" do
285       SimpleJob.runs.should == 0     
286       @job.stub!(:lock_exclusively!).with(any_args).once.and_return(false)
287       Delayed::Job.should_receive(:find_available).once.and_return([@job])
288       Delayed::Job.work_off(1)
289       SimpleJob.runs.should == 0
290     end
291   
292   end
293   
294   context "while running alongside other workers that locked jobs, it" do
295     before(:each) do
296       Delayed::Job.worker_name = 'worker1'
297       Delayed::Job.create(:payload_object => SimpleJob.new, :locked_by => 'worker1', :locked_at => (Delayed::Job.db_time_now - 1.minutes))
298       Delayed::Job.create(:payload_object => SimpleJob.new, :locked_by => 'worker2', :locked_at => (Delayed::Job.db_time_now - 1.minutes))
299       Delayed::Job.create(:payload_object => SimpleJob.new)
300       Delayed::Job.create(:payload_object => SimpleJob.new, :locked_by => 'worker1', :locked_at => (Delayed::Job.db_time_now - 1.minutes))
301     end
302
303     it "should ingore locked jobs from other workers" do
304       Delayed::Job.worker_name = 'worker3'
305       SimpleJob.runs.should == 0
306       Delayed::Job.work_off
307       SimpleJob.runs.should == 1 # runs the one open job
308     end
309
310     it "should find our own jobs regardless of locks" do
311       Delayed::Job.worker_name = 'worker1'
312       SimpleJob.runs.should == 0
313       Delayed::Job.work_off
314       SimpleJob.runs.should == 3 # runs open job plus worker1 jobs that were already locked
315     end
316   end
317
318   context "while running with locked and expired jobs, it" do
319     before(:each) do
320       Delayed::Job.worker_name = 'worker1'
321       exp_time = Delayed::Job.db_time_now - (1.minutes + Delayed::Job::MAX_RUN_TIME)
322       Delayed::Job.create(:payload_object => SimpleJob.new, :locked_by => 'worker1', :locked_at => exp_time)
323       Delayed::Job.create(:payload_object => SimpleJob.new, :locked_by => 'worker2', :locked_at => (Delayed::Job.db_time_now - 1.minutes))
324       Delayed::Job.create(:payload_object => SimpleJob.new)
325       Delayed::Job.create(:payload_object => SimpleJob.new, :locked_by => 'worker1', :locked_at => (Delayed::Job.db_time_now - 1.minutes))
326     end
327
328     it "should only find unlocked and expired jobs" do
329       Delayed::Job.worker_name = 'worker3'
330       SimpleJob.runs.should == 0
331       Delayed::Job.work_off
332       SimpleJob.runs.should == 2 # runs the one open job and one expired job
333     end
334
335     it "should ignore locks when finding our own jobs" do
336       Delayed::Job.worker_name = 'worker1'
337       SimpleJob.runs.should == 0
338       Delayed::Job.work_off
339       SimpleJob.runs.should == 3 # runs open job plus worker1 jobs
340       # This is useful in the case of a crash/restart on worker1, but make sure multiple workers on the same host have unique names!
341     end
342
343   end
344   
345 end