forked from tobi/delayed_job
/
shared_backend_spec.rb
352 lines (284 loc) · 11.1 KB
/
shared_backend_spec.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
class NamedJob < Struct.new(:perform)
def display_name
'named_job'
end
end
class SuccessfulCallbackJob
def before(job)
SuccessfulCallbackJob.messages << 'before perform'
end
def perform
SuccessfulCallbackJob.messages << 'perform'
end
def after(job, error = nil)
SuccessfulCallbackJob.messages << 'after perform'
end
def success(job)
SuccessfulCallbackJob.messages << 'success!'
end
def failure(job, error)
SuccessfulCallbackJob.messages << 'oops!'
end
class << self
attr_accessor :messages
end
end
class FailureCallbackJob
def before(job)
FailureCallbackJob.messages << 'before perform'
end
def perform
1 / nil
end
def after(job)
FailureCallbackJob.messages << "after perform"
end
def success(job)
FailureCallbackJob.messages << 'success!'
end
def failure(job, error)
FailureCallbackJob.messages << "error during peform: #{error.message}"
end
class << self
attr_accessor :messages
end
end
shared_examples_for 'a backend' do
def create_job(opts = {})
@backend.create(opts.merge(:payload_object => SimpleJob.new))
end
before do
Delayed::Worker.max_priority = nil
Delayed::Worker.min_priority = nil
Delayed::Worker.default_priority = 99
SimpleJob.runs = 0
end
it "should set run_at automatically if not set" do
@backend.create(:payload_object => ErrorJob.new ).run_at.should_not be_nil
end
it "should not set run_at automatically if already set" do
later = @backend.db_time_now + 5.minutes
@backend.create(:payload_object => ErrorJob.new, :run_at => later).run_at.should be_close(later, 1)
end
it "should raise ArgumentError when handler doesn't respond_to :perform" do
lambda { @backend.enqueue(Object.new) }.should raise_error(ArgumentError)
end
it "should increase count after enqueuing items" do
@backend.enqueue SimpleJob.new
@backend.count.should == 1
end
it "should be able to set priority when enqueuing items" do
@job = @backend.enqueue SimpleJob.new, 5
@job.priority.should == 5
end
it "should use default priority when it is not set" do
@job = @backend.enqueue SimpleJob.new
@job.priority.should == 99
end
it "should be able to set run_at when enqueuing items" do
later = @backend.db_time_now + 5.minutes
@job = @backend.enqueue SimpleJob.new, 5, later
@job.run_at.should be_close(later, 1)
end
it "should work with jobs in modules" do
M::ModuleJob.runs = 0
job = @backend.enqueue M::ModuleJob.new
lambda { job.invoke_job }.should change { M::ModuleJob.runs }.from(0).to(1)
end
describe "callbacks" do
before(:each) do
SuccessfulCallbackJob.messages = []
FailureCallbackJob.messages = []
end
it "should call before and after callbacks" do
job = @backend.enqueue(SuccessfulCallbackJob.new)
job.invoke_job
SuccessfulCallbackJob.messages.should == ["before perform", "perform", "success!", "after perform"]
end
it "should call the after callback with an error" do
job = @backend.enqueue(FailureCallbackJob.new)
lambda {job.invoke_job}.should raise_error(TypeError)
FailureCallbackJob.messages.should == ["before perform", "error during peform: nil can't be coerced into Fixnum", "after perform"]
end
end
describe "payload_object" do
it "should raise a DeserializationError when the job class is totally unknown" do
job = @backend.new :handler => "--- !ruby/object:JobThatDoesNotExist {}"
lambda { job.payload_object }.should raise_error(Delayed::Backend::DeserializationError)
end
it "should raise a DeserializationError when the job struct is totally unknown" do
job = @backend.new :handler => "--- !ruby/struct:StructThatDoesNotExist {}"
lambda { job.payload_object }.should raise_error(Delayed::Backend::DeserializationError)
end
it "should autoload classes that are unknown at runtime" do
job = @backend.new :handler => "--- !ruby/object:Autoloaded::Clazz {}"
lambda { job.payload_object }.should_not raise_error(Delayed::Backend::DeserializationError)
end
it "should autoload structs that are unknown at runtime" do
job = @backend.new :handler => "--- !ruby/struct:Autoloaded::Struct {}"
lambda { job.payload_object }.should_not raise_error(Delayed::Backend::DeserializationError)
end
end
describe "find_available" do
it "should not find failed jobs" do
@job = create_job :attempts => 50, :failed_at => @backend.db_time_now
@backend.find_available('worker', 5, 1.second).should_not include(@job)
end
it "should not find jobs scheduled for the future" do
@job = create_job :run_at => (@backend.db_time_now + 1.minute)
@backend.find_available('worker', 5, 4.hours).should_not include(@job)
end
it "should not find jobs locked by another worker" do
@job = create_job(:locked_by => 'other_worker', :locked_at => @backend.db_time_now - 1.minute)
@backend.find_available('worker', 5, 4.hours).should_not include(@job)
end
it "should find open jobs" do
@job = create_job
@backend.find_available('worker', 5, 4.hours).should include(@job)
end
it "should find expired jobs" do
@job = create_job(:locked_by => 'worker', :locked_at => @backend.db_time_now - 2.minutes)
@backend.find_available('worker', 5, 1.minute).should include(@job)
end
it "should find own jobs" do
@job = create_job(:locked_by => 'worker', :locked_at => (@backend.db_time_now - 1.minutes))
@backend.find_available('worker', 5, 4.hours).should include(@job)
end
it "should find only the right amount of jobs" do
10.times { create_job }
@backend.find_available('worker', 7, 4.hours).should have(7).jobs
end
end
context "when another worker is already performing an task, it" do
before :each do
@job = @backend.create :payload_object => SimpleJob.new, :locked_by => 'worker1', :locked_at => @backend.db_time_now - 5.minutes
end
it "should not allow a second worker to get exclusive access" do
@job.lock_exclusively!(4.hours, 'worker2').should == false
end
it "should allow a second worker to get exclusive access if the timeout has passed" do
@job.lock_exclusively!(1.minute, 'worker2').should == true
end
it "should be able to get access to the task if it was started more then max_age ago" do
@job.locked_at = 5.hours.ago
@job.save
@job.lock_exclusively! 4.hours, 'worker2'
@job.reload
@job.locked_by.should == 'worker2'
@job.locked_at.should > 1.minute.ago
end
it "should not be found by another worker" do
@backend.find_available('worker2', 1, 6.minutes).length.should == 0
end
it "should be found by another worker if the time has expired" do
@backend.find_available('worker2', 1, 4.minutes).length.should == 1
end
it "should be able to get exclusive access again when the worker name is the same" do
@job.lock_exclusively!(5.minutes, 'worker1').should be_true
@job.lock_exclusively!(5.minutes, 'worker1').should be_true
@job.lock_exclusively!(5.minutes, 'worker1').should be_true
end
end
context "when another worker has worked on a task since the job was found to be available, it" do
before :each do
@job = @backend.create :payload_object => SimpleJob.new
@job_copy_for_worker_2 = @backend.find(@job.id)
end
it "should not allow a second worker to get exclusive access if already successfully processed by worker1" do
@job.destroy
@job_copy_for_worker_2.lock_exclusively!(4.hours, 'worker2').should == false
end
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
@job.update_attributes(:attempts => 1, :run_at => 1.day.from_now)
@job_copy_for_worker_2.lock_exclusively!(4.hours, 'worker2').should == false
end
end
context "#name" do
it "should be the class name of the job that was enqueued" do
@backend.create(:payload_object => ErrorJob.new ).name.should == 'ErrorJob'
end
it "should be the method that will be called if its a performable method object" do
job = @backend.new(:payload_object => NamedJob.new)
job.name.should == 'named_job'
end
it "should be the instance method that will be called if its a performable method object" do
@job = Story.create(:text => "...").delay.save
@job.name.should == 'Story#save'
end
end
context "worker prioritization" do
before(:each) do
Delayed::Worker.max_priority = nil
Delayed::Worker.min_priority = nil
end
it "should fetch jobs ordered by priority" do
10.times { @backend.enqueue SimpleJob.new, rand(10) }
jobs = @backend.find_available('worker', 10)
jobs.size.should == 10
jobs.each_cons(2) do |a, b|
a.priority.should <= b.priority
end
end
it "should only find jobs greater than or equal to min priority" do
min = 5
Delayed::Worker.min_priority = min
10.times {|i| @backend.enqueue SimpleJob.new, i }
jobs = @backend.find_available('worker', 10)
jobs.each {|job| job.priority.should >= min}
end
it "should only find jobs less than or equal to max priority" do
max = 5
Delayed::Worker.max_priority = max
10.times {|i| @backend.enqueue SimpleJob.new, i }
jobs = @backend.find_available('worker', 10)
jobs.each {|job| job.priority.should <= max}
end
end
context "clear_locks!" do
before do
@job = create_job(:locked_by => 'worker', :locked_at => @backend.db_time_now)
end
it "should clear locks for the given worker" do
@backend.clear_locks!('worker')
@backend.find_available('worker2', 5, 1.minute).should include(@job)
end
it "should not clear locks for other workers" do
@backend.clear_locks!('worker1')
@backend.find_available('worker1', 5, 1.minute).should_not include(@job)
end
end
context "unlock" do
before do
@job = create_job(:locked_by => 'worker', :locked_at => @backend.db_time_now)
end
it "should clear locks" do
@job.unlock
@job.locked_by.should be_nil
@job.locked_at.should be_nil
end
end
context "large handler" do
before do
text = "Lorem ipsum dolor sit amet. " * 1000
@job = @backend.enqueue Delayed::PerformableMethod.new(text, :length, {})
end
it "should have an id" do
@job.id.should_not be_nil
end
end
describe "yaml serialization" do
it "should reload changed attributes" do
job = @backend.enqueue SimpleJob.new
yaml = job.to_yaml
job.priority = 99
job.save
YAML.load(yaml).priority.should == 99
end
it "should ignore destroyed records" do
job = @backend.enqueue SimpleJob.new
yaml = job.to_yaml
job.destroy
lambda { YAML.load(yaml).should be_nil }.should_not raise_error
end
end
end