sidejob 3.0.0
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.
- checksums.yaml +7 -0
- data/.gitignore +5 -0
- data/.rspec +2 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +61 -0
- data/README.md +128 -0
- data/bin/build +11 -0
- data/bin/console +7 -0
- data/lib/sidejob/job.rb +425 -0
- data/lib/sidejob/port.rb +206 -0
- data/lib/sidejob/server_middleware.rb +117 -0
- data/lib/sidejob/testing.rb +54 -0
- data/lib/sidejob/version.rb +4 -0
- data/lib/sidejob/worker.rb +133 -0
- data/lib/sidejob.rb +116 -0
- data/sidejob.gemspec +21 -0
- data/spec/integration/fib_spec.rb +51 -0
- data/spec/integration/sum_spec.rb +39 -0
- data/spec/sidejob/job_spec.rb +543 -0
- data/spec/sidejob/port_spec.rb +452 -0
- data/spec/sidejob/server_middleware_spec.rb +254 -0
- data/spec/sidejob/testing_spec.rb +108 -0
- data/spec/sidejob/worker_spec.rb +201 -0
- data/spec/sidejob_spec.rb +182 -0
- data/spec/spec_helper.rb +21 -0
- data/spec/support/test_sum.rb +17 -0
- data/spec/support/test_worker.rb +6 -0
- metadata +140 -0
@@ -0,0 +1,452 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe SideJob::Port do
|
4
|
+
before do
|
5
|
+
@job = SideJob.queue('testq', 'TestWorker', inports: {
|
6
|
+
port1: {},
|
7
|
+
memory: { mode: :memory },
|
8
|
+
default: { default: 'default' },
|
9
|
+
default_null: { default: nil },
|
10
|
+
default_false: { default: false},
|
11
|
+
memory_with_default: { mode: :memory, default: 'memory default' },
|
12
|
+
}, outports: {
|
13
|
+
out1: {},
|
14
|
+
})
|
15
|
+
@port1 = @job.input(:port1)
|
16
|
+
@out1 = @job.output(:out1)
|
17
|
+
@memory = @job.input(:memory)
|
18
|
+
@default = @job.input(:default)
|
19
|
+
@defaults = [
|
20
|
+
@default,
|
21
|
+
@job.input(:default_null),
|
22
|
+
@job.input(:default_false),
|
23
|
+
@job.input(:memory_with_default)
|
24
|
+
]
|
25
|
+
end
|
26
|
+
|
27
|
+
describe '#initialize' do
|
28
|
+
it 'raises error if name is invalid' do
|
29
|
+
expect { SideJob::Port.new(@job, :in, 'PORT.1')}.to raise_error
|
30
|
+
end
|
31
|
+
|
32
|
+
it 'raises error if name is empty' do
|
33
|
+
expect { SideJob::Port.new(@job, :in, '')}.to raise_error
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
describe '#==, #eql?' do
|
38
|
+
it 'two ports with the same job, type, and name are eq' do
|
39
|
+
expect(SideJob::Port.new(@job, :in, :port1)).to eq(@port1)
|
40
|
+
expect(SideJob::Port.new(@job, :in, 'port1')).to eq(@port1)
|
41
|
+
expect(SideJob::Port.new(@job, :in, 'port1')).to eql(@port1)
|
42
|
+
end
|
43
|
+
|
44
|
+
it 'two ports with different names are not eq' do
|
45
|
+
expect(SideJob::Port.new(@job, :in, 'port2')).not_to eq(@port1)
|
46
|
+
expect(SideJob::Port.new(@job, :in, 'port2')).not_to eql(@port1)
|
47
|
+
end
|
48
|
+
|
49
|
+
it 'port names are case sensitive' do
|
50
|
+
expect(SideJob::Port.new(@job, :in, 'PORT1')).not_to eq(@port1)
|
51
|
+
expect(SideJob::Port.new(@job, :in, 'PORT1')).not_to eql(@port1)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
describe '#to_s' do
|
56
|
+
it 'returns the redis key' do
|
57
|
+
expect(SideJob::Port.new(@job, :in, :port1).to_s).to eq "job:#{@job.id}:in:port1"
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
describe '#options' do
|
62
|
+
it 'returns port options' do
|
63
|
+
expect(@port1.options).to eq({mode: :queue})
|
64
|
+
expect(@memory.options).to eq({mode: :memory})
|
65
|
+
expect(@default.options).to eq({mode: :queue, default: 'default'})
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
describe '#options=' do
|
70
|
+
it 'can change mode to memory' do
|
71
|
+
expect(@port1.mode).to eq :queue
|
72
|
+
@port1.options = { mode: :memory }
|
73
|
+
expect(@port1.mode).to eq :memory
|
74
|
+
end
|
75
|
+
|
76
|
+
it 'can change mode to queue' do
|
77
|
+
expect(@memory.mode).to eq :memory
|
78
|
+
@memory.options = { mode: :queue }
|
79
|
+
expect(@memory.mode).to eq :queue
|
80
|
+
end
|
81
|
+
|
82
|
+
it 'can set default value' do
|
83
|
+
expect(@port1.default?).to be false
|
84
|
+
@port1.options = { default: [1,2] }
|
85
|
+
expect(@port1.default).to eq [1,2]
|
86
|
+
expect(@port1.read).to eq [1,2]
|
87
|
+
end
|
88
|
+
|
89
|
+
it 'can remove default value' do
|
90
|
+
expect(@default.default?).to be true
|
91
|
+
@default.options = { }
|
92
|
+
expect(@default.default?).to be false
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
describe '#mode' do
|
97
|
+
it 'returns nil for non-existent ports' do
|
98
|
+
expect(SideJob::Port.new(@job, :in, 'missing').mode).to be nil
|
99
|
+
end
|
100
|
+
|
101
|
+
it 'returns the port mode for queue ports' do
|
102
|
+
expect(@port1.mode).to eq :queue
|
103
|
+
expect(@default.mode).to eq :queue
|
104
|
+
end
|
105
|
+
|
106
|
+
it 'returns the port mode for memory ports' do
|
107
|
+
expect(@memory.mode).to eq :memory
|
108
|
+
expect(@job.input(:memory_with_default).mode).to eq :memory
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
describe '#size' do
|
113
|
+
it 'returns 0 when the port is empty' do
|
114
|
+
expect(@port1.size).to eq 0
|
115
|
+
end
|
116
|
+
|
117
|
+
it 'number of elements is increased on write' do
|
118
|
+
expect {
|
119
|
+
@port1.write 1
|
120
|
+
}.to change(@port1, :size).by(1)
|
121
|
+
end
|
122
|
+
|
123
|
+
it 'number of elements is decreased on read' do
|
124
|
+
@port1.write 1
|
125
|
+
expect {
|
126
|
+
@port1.read
|
127
|
+
}.to change(@port1, :size).by(-1)
|
128
|
+
end
|
129
|
+
|
130
|
+
it 'returns 0 if there is default value' do
|
131
|
+
@defaults.each {|port| expect(port.size).to eq 0 }
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
describe '#data?' do
|
136
|
+
it 'returns false when the port is empty' do
|
137
|
+
expect(@port1.data?).to be false
|
138
|
+
end
|
139
|
+
|
140
|
+
it 'returns true when the port has data' do
|
141
|
+
@port1.write 1
|
142
|
+
expect(@port1.data?).to be true
|
143
|
+
end
|
144
|
+
|
145
|
+
it 'works for memory ports' do
|
146
|
+
expect(@memory.data?).to be false
|
147
|
+
@memory.write 1
|
148
|
+
expect(@memory.data?).to be true
|
149
|
+
end
|
150
|
+
|
151
|
+
it 'works when there is a default value on the port' do
|
152
|
+
@defaults.each {|port| expect(port.data?).to be true }
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
describe '#default' do
|
157
|
+
it 'returns nil for no default' do
|
158
|
+
expect(@port1.default).to be nil
|
159
|
+
end
|
160
|
+
|
161
|
+
it 'returns default value' do
|
162
|
+
@port1.options = {default: [1,2]}
|
163
|
+
expect(@port1.default).to eq [1,2]
|
164
|
+
end
|
165
|
+
|
166
|
+
it 'can return null default value' do
|
167
|
+
@port1.options = {default: nil}
|
168
|
+
expect(@port1.default).to be nil
|
169
|
+
expect(@port1.default?).to be true
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
describe '#default?' do
|
174
|
+
it 'returns false for normal port' do
|
175
|
+
expect(@port1.default?).to be false
|
176
|
+
end
|
177
|
+
|
178
|
+
it 'returns false for memory port with no data written' do
|
179
|
+
expect(@memory.default?).to be false
|
180
|
+
end
|
181
|
+
|
182
|
+
it 'returns true for memory port after writing data' do
|
183
|
+
@memory.write 1
|
184
|
+
expect(@memory.default?).to be true
|
185
|
+
end
|
186
|
+
|
187
|
+
it 'returns true for port with default value' do
|
188
|
+
@defaults.each {|port| expect(port.default?).to be true }
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
describe '#write' do
|
193
|
+
it 'can write different types of data to a port' do
|
194
|
+
['abc', 123, true, false, nil, {abc: 123}, [1, {foo: true}]].each {|x| @port1.write x}
|
195
|
+
data = SideJob.redis.lrange(@port1.redis_key, 0, -1)
|
196
|
+
expect(data).to eq(['"abc"', '123', 'true', 'false', 'null', '{"abc":123}', '[1,{"foo":true}]'])
|
197
|
+
end
|
198
|
+
|
199
|
+
it 'writing to a memory port should only set default value to latest value' do
|
200
|
+
@memory.write 'abc'
|
201
|
+
@memory.write [1, {foo: true}]
|
202
|
+
@memory.write 'latest'
|
203
|
+
expect(@memory.size).to eq 0
|
204
|
+
expect(@memory.default).to eq 'latest'
|
205
|
+
end
|
206
|
+
|
207
|
+
it 'logs writes' do
|
208
|
+
now = Time.now
|
209
|
+
allow(Time).to receive(:now) { now }
|
210
|
+
@job.group_port_logs do
|
211
|
+
@port1.write 'abc'
|
212
|
+
@port1.write 123
|
213
|
+
end
|
214
|
+
expect(SideJob.logs).to eq [{'timestamp' => SideJob.timestamp, 'job' => @job.id, 'read' => [],
|
215
|
+
'write' => ['job' => @job.id, 'inport' => 'port1', 'data' => ['abc', 123]]}]
|
216
|
+
end
|
217
|
+
|
218
|
+
it 'raises error if port does not exist' do
|
219
|
+
expect { SideJob::Port.new(@job, :in, 'foo').write true }.to raise_error
|
220
|
+
end
|
221
|
+
|
222
|
+
it 'raises error if port mode is unknown ' do
|
223
|
+
SideJob.redis.hset "#{@job.redis_key}:inports:mode", 'foo', '???'
|
224
|
+
expect { SideJob::Port.new(@job, :in, 'foo').write true }.to raise_error
|
225
|
+
end
|
226
|
+
|
227
|
+
it 'runs the job if it is an input port' do
|
228
|
+
expect(@port1.job).to receive(:run)
|
229
|
+
@port1.write 3
|
230
|
+
end
|
231
|
+
|
232
|
+
it 'does not run the job if it is an output port' do
|
233
|
+
expect(@out1.job).not_to receive(:run)
|
234
|
+
@out1.write 3
|
235
|
+
end
|
236
|
+
|
237
|
+
it 'does not run the job if it is a memory port' do
|
238
|
+
expect(@memory.job).not_to receive(:run)
|
239
|
+
@memory.write 3
|
240
|
+
end
|
241
|
+
end
|
242
|
+
|
243
|
+
describe '#read' do
|
244
|
+
it 'can distinguish reading nil data and no data' do
|
245
|
+
expect { @port1.read }.to raise_error(EOFError)
|
246
|
+
@port1.write nil
|
247
|
+
expect(@port1.read).to be nil
|
248
|
+
end
|
249
|
+
|
250
|
+
it 'can read data from a queue port' do
|
251
|
+
expect { @port1.read }.to raise_error(EOFError)
|
252
|
+
['abc', 123, true, false, nil, {}, ['data1', 1, {key: 'val'}]].each {|x| @port1.write x}
|
253
|
+
expect(@port1.size).to be(7)
|
254
|
+
expect(@port1.read).to eq('abc')
|
255
|
+
expect(@port1.read).to eq(123)
|
256
|
+
expect(@port1.read).to eq(true)
|
257
|
+
expect(@port1.read).to eq(false)
|
258
|
+
expect(@port1.read).to eq(nil)
|
259
|
+
expect(@port1.read).to eq({})
|
260
|
+
expect(@port1.read).to eq(['data1', 1, {'key' => 'val'}])
|
261
|
+
expect { @port1.read }.to raise_error(EOFError)
|
262
|
+
expect(@port1.size).to be(0)
|
263
|
+
end
|
264
|
+
|
265
|
+
it 'can read data from a memory port' do
|
266
|
+
expect { @memory.read }.to raise_error(EOFError)
|
267
|
+
5.times {|i| @memory.write i }
|
268
|
+
3.times { expect(@memory.read).to eq(4) }
|
269
|
+
end
|
270
|
+
|
271
|
+
it 'can use default value' do
|
272
|
+
@defaults.each do |port|
|
273
|
+
3.times { expect(port.read).to eq port.default }
|
274
|
+
port.write 'mydata'
|
275
|
+
expect(port.read).to eq 'mydata'
|
276
|
+
3.times { expect(port.read).to eq port.mode == :memory ? 'mydata' : port.default }
|
277
|
+
end
|
278
|
+
end
|
279
|
+
|
280
|
+
it 'can use null default value' do
|
281
|
+
port = @job.input(:default_null)
|
282
|
+
expect(port.read).to eq nil
|
283
|
+
end
|
284
|
+
|
285
|
+
it 'can use false default value' do
|
286
|
+
port = @job.input(:default_false)
|
287
|
+
expect(port.read).to be false
|
288
|
+
end
|
289
|
+
|
290
|
+
it 'logs reads' do
|
291
|
+
now = Time.now
|
292
|
+
allow(Time).to receive(:now) { now }
|
293
|
+
['abc', 123].each {|x| @port1.write x}
|
294
|
+
SideJob.logs
|
295
|
+
@job.group_port_logs do
|
296
|
+
expect(@port1.read).to eq('abc')
|
297
|
+
expect(@port1.read).to eq(123)
|
298
|
+
end
|
299
|
+
expect(SideJob.logs).to eq [{'timestamp' => SideJob.timestamp, 'job' => @job.id,
|
300
|
+
'read' => ['job' => @job.id, 'inport' => 'port1', 'data' => ['abc', 123]],
|
301
|
+
'write' => []}]
|
302
|
+
end
|
303
|
+
end
|
304
|
+
|
305
|
+
describe '#connect_to' do
|
306
|
+
it 'does nothing on an empty port' do
|
307
|
+
expect(@out1.connect_to(@port1)).to eq []
|
308
|
+
expect(@port1.data?).to be false
|
309
|
+
end
|
310
|
+
|
311
|
+
it 'sends data to a port' do
|
312
|
+
@out1.write 1
|
313
|
+
@out1.write [2,3]
|
314
|
+
expect(@out1.connect_to(@port1)).to eq [1, [2,3]]
|
315
|
+
expect(@out1.data?).to be false
|
316
|
+
expect(@port1.read).to eq 1
|
317
|
+
expect(@port1.read).to eq [2,3]
|
318
|
+
expect(@port1.data?).to be false
|
319
|
+
end
|
320
|
+
|
321
|
+
it 'sends data to all destination ports' do
|
322
|
+
dest = [@port1, @memory, @default]
|
323
|
+
@out1.write 1
|
324
|
+
@out1.write [2,3]
|
325
|
+
@out1.connect_to dest
|
326
|
+
expect(@port1.read).to eq 1
|
327
|
+
expect(@port1.read).to eq [2,3]
|
328
|
+
expect(@port1.data?).to be false
|
329
|
+
expect(@memory.size).to eq 0
|
330
|
+
expect(@memory.read).to eq [2,3]
|
331
|
+
expect(@default.read).to eq 1
|
332
|
+
expect(@default.read).to eq [2,3]
|
333
|
+
expect(@default.read).to eq 'default'
|
334
|
+
end
|
335
|
+
|
336
|
+
it 'passes port default values to all destinations' do
|
337
|
+
dest = [@port1, @memory, @out1]
|
338
|
+
@default.write 1
|
339
|
+
@default.write [2,3]
|
340
|
+
@default.connect_to dest
|
341
|
+
expect(@port1.read).to eq 1
|
342
|
+
expect(@port1.read).to eq [2,3]
|
343
|
+
expect(@port1.default).to eq 'default'
|
344
|
+
expect(@memory.size).to eq 0
|
345
|
+
expect(@memory.default).to eq 'default'
|
346
|
+
expect(@out1.read).to eq 1
|
347
|
+
expect(@out1.read).to eq [2,3]
|
348
|
+
expect(@out1.default).to eq 'default'
|
349
|
+
end
|
350
|
+
|
351
|
+
it 'runs job for normal input port' do
|
352
|
+
expect(@port1.job).to receive(:run)
|
353
|
+
@out1.write true
|
354
|
+
@out1.connect_to @port1
|
355
|
+
end
|
356
|
+
|
357
|
+
it 'does not run job if no data sent' do
|
358
|
+
expect(@port1.job).not_to receive(:run)
|
359
|
+
@out1.connect_to @port1
|
360
|
+
end
|
361
|
+
|
362
|
+
it 'does not run job for memory port' do
|
363
|
+
expect(@memory.job).not_to receive(:run)
|
364
|
+
@out1.write true
|
365
|
+
@out1.connect_to @memory
|
366
|
+
end
|
367
|
+
|
368
|
+
it 'logs data' do
|
369
|
+
now = Time.now
|
370
|
+
allow(Time).to receive(:now) { now }
|
371
|
+
@out1.write 1
|
372
|
+
@out1.write [2,3]
|
373
|
+
SideJob.logs(clear: true)
|
374
|
+
@out1.connect_to(@port1)
|
375
|
+
expect(SideJob.logs).to eq([{'timestamp' => SideJob.timestamp,
|
376
|
+
'read' => [{'job' => @job.id, 'outport' => 'out1', 'data' => [1,[2,3]]}],
|
377
|
+
'write' => [{'job' => @job.id, 'inport' => 'port1', 'data' => [1,[2,3]]}] }])
|
378
|
+
end
|
379
|
+
|
380
|
+
it 'can specify additional metadata to add to log' do
|
381
|
+
now = Time.now
|
382
|
+
allow(Time).to receive(:now) { now }
|
383
|
+
job2 = SideJob.queue('testq', 'TestWorker')
|
384
|
+
@out1.write 1
|
385
|
+
@out1.write [2,3]
|
386
|
+
SideJob.logs(clear: true)
|
387
|
+
@out1.connect_to(@port1, user: 'test')
|
388
|
+
expect(SideJob.logs).to eq([{'timestamp' => SideJob.timestamp, 'user' => 'test',
|
389
|
+
'read' => [{'job' => @job.id, 'outport' => 'out1', 'data' => [1,[2,3]]}],
|
390
|
+
'write' => [{'job' => @job.id, 'inport' => 'port1', 'data' => [1,[2,3]]}] }])
|
391
|
+
end
|
392
|
+
|
393
|
+
it 'does not log if no data on port' do
|
394
|
+
job2 = SideJob.queue('testq', 'TestWorker')
|
395
|
+
@out1.connect_to(@port1)
|
396
|
+
expect(SideJob.logs).to eq([])
|
397
|
+
end
|
398
|
+
end
|
399
|
+
|
400
|
+
describe 'is Enumerable' do
|
401
|
+
it 'can iterate over port elements' do
|
402
|
+
10.times {|i| @port1.write i}
|
403
|
+
num = 0
|
404
|
+
@port1.each_with_index do |elem, i|
|
405
|
+
expect(elem).to eq i
|
406
|
+
num += 1
|
407
|
+
end
|
408
|
+
expect(num).to eq 10
|
409
|
+
end
|
410
|
+
|
411
|
+
it '#entries returns all data as an array' do
|
412
|
+
expect(@port1.entries).to eq []
|
413
|
+
10.times {|i| @port1.write i}
|
414
|
+
expect(@port1.entries).to eq Array(0..9)
|
415
|
+
expect(@port1.entries).to eq []
|
416
|
+
end
|
417
|
+
|
418
|
+
it 'default values are not returned' do
|
419
|
+
@defaults.each do |port|
|
420
|
+
expect(port.entries).to eq []
|
421
|
+
port.write 'mydata'
|
422
|
+
if port.mode == :memory
|
423
|
+
expect(port.entries).to eq []
|
424
|
+
else
|
425
|
+
expect(port.entries).to eq ['mydata']
|
426
|
+
end
|
427
|
+
end
|
428
|
+
end
|
429
|
+
end
|
430
|
+
|
431
|
+
describe '#redis_key' do
|
432
|
+
it 'returns key with valid name' do
|
433
|
+
expect(@port1.redis_key).to eq("#{@port1.job.redis_key}:in:port1")
|
434
|
+
end
|
435
|
+
end
|
436
|
+
|
437
|
+
describe '#hash' do
|
438
|
+
it 'uses hash of the redis key' do
|
439
|
+
expect(@port1.hash).to eq(@port1.redis_key.hash)
|
440
|
+
end
|
441
|
+
|
442
|
+
it 'can be used as keys in a hash' do
|
443
|
+
h = {}
|
444
|
+
h[@port1] = 1
|
445
|
+
port2 = SideJob::Port.new(@job, :in, 'port1')
|
446
|
+
expect(@port1.hash).to eq(port2.hash)
|
447
|
+
h[port2] = 3
|
448
|
+
expect(h.keys.length).to be(1)
|
449
|
+
expect(h[@port1]).to be(3)
|
450
|
+
end
|
451
|
+
end
|
452
|
+
end
|
@@ -0,0 +1,254 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe SideJob::ServerMiddleware do
|
4
|
+
class TestWorkerShutdown
|
5
|
+
include SideJob::Worker
|
6
|
+
register
|
7
|
+
attr_accessor :shutdown_called
|
8
|
+
def shutdown
|
9
|
+
@shutdown_called = true
|
10
|
+
end
|
11
|
+
def perform
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
class TestWorkerShutdownError
|
16
|
+
include SideJob::Worker
|
17
|
+
register
|
18
|
+
def shutdown
|
19
|
+
raise 'shutdown error'
|
20
|
+
end
|
21
|
+
def perform
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
before do
|
26
|
+
@queue = 'testq'
|
27
|
+
@job = SideJob.queue(@queue, 'TestWorker')
|
28
|
+
end
|
29
|
+
|
30
|
+
def process(job)
|
31
|
+
chain = Sidekiq::Middleware::Chain.new
|
32
|
+
chain.add SideJob::ServerMiddleware
|
33
|
+
msg = Sidekiq::Queue.new(@queue).find_job(job.id)
|
34
|
+
worker = msg.klass.constantize.new
|
35
|
+
worker.jid = job.id
|
36
|
+
chain.invoke(worker, msg, @queue) { yield worker }
|
37
|
+
job.reload
|
38
|
+
worker
|
39
|
+
end
|
40
|
+
|
41
|
+
%w{running suspended completed failed terminated}.each do |status|
|
42
|
+
it "does not run if status is #{status}" do
|
43
|
+
@job.status = status
|
44
|
+
@run = false
|
45
|
+
process(@job) { @run = true}
|
46
|
+
expect(@run).to be false
|
47
|
+
expect(@job.status).to eq status
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
it 'does not run if job has been deleted' do
|
52
|
+
@job.status = 'terminated'
|
53
|
+
@job.delete
|
54
|
+
@run = false
|
55
|
+
process(@job) { @run = true}
|
56
|
+
expect(@run).to be false
|
57
|
+
end
|
58
|
+
|
59
|
+
describe 'handles a normal run' do
|
60
|
+
it 'sets status to running on start and completed on completion' do
|
61
|
+
process(@job) { |worker| @status = worker.status }
|
62
|
+
expect(@status).to eq 'running'
|
63
|
+
expect(@job.status).to eq 'completed'
|
64
|
+
end
|
65
|
+
|
66
|
+
it 'runs the parent job' do
|
67
|
+
@job.status = 'suspended'
|
68
|
+
child = SideJob.queue(@queue, 'TestWorker', parent: @job, name: 'child')
|
69
|
+
expect(@job.status).to eq 'suspended'
|
70
|
+
expect {
|
71
|
+
child.run_inline
|
72
|
+
}.to change {Sidekiq::Stats.new.enqueued}.by(1)
|
73
|
+
@job.reload
|
74
|
+
expect(@job.status).to eq 'queued'
|
75
|
+
end
|
76
|
+
|
77
|
+
it 'sets the ran_at time at the beginning of the run' do
|
78
|
+
now = Time.now
|
79
|
+
allow(Time).to receive(:now) { now }
|
80
|
+
process(@job) { |worker| @ran_at = worker.get(:ran_at) }
|
81
|
+
expect(@ran_at).to eq SideJob.timestamp
|
82
|
+
expect(@job.status).to eq 'completed'
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
describe 'prevents multiple threads running the same job' do
|
87
|
+
it 'sets the job lock to the current time' do
|
88
|
+
now = Time.now
|
89
|
+
allow(Time).to receive(:now) { now }
|
90
|
+
process(@job) { @lock = SideJob.redis.get("#{@job.redis_key}:lock").to_f }
|
91
|
+
expect(@lock).to eq(now.to_f)
|
92
|
+
expect(SideJob.redis.exists("#{@job.redis_key}:lock")).to be false
|
93
|
+
end
|
94
|
+
|
95
|
+
it 'sets the job lock to the current time and does not run if already locked' do
|
96
|
+
now = Time.now
|
97
|
+
allow(Time).to receive(:now) { now }
|
98
|
+
@run = false
|
99
|
+
SideJob.redis.set "#{@job.redis_key}:lock", (now-10).to_f
|
100
|
+
process(@job) { @run = true }
|
101
|
+
expect(@run).to be false
|
102
|
+
expect(SideJob.redis.get("#{@job.redis_key}:lock").to_f).to eq now.to_f
|
103
|
+
end
|
104
|
+
|
105
|
+
it 'does not do anything if the enqueued_at time is before the ran_at' do
|
106
|
+
process(@job) {|worker| worker.set ran_at: SideJob.timestamp}
|
107
|
+
@run = false
|
108
|
+
process(@job) {@run = true}
|
109
|
+
expect(@run).to be false
|
110
|
+
end
|
111
|
+
|
112
|
+
it 'does not restart the worker unless another worker was locked out during the run' do
|
113
|
+
expect {
|
114
|
+
process(@job) {}
|
115
|
+
}.to change {Sidekiq::Stats.new.enqueued}.by(0)
|
116
|
+
expect(@job.status).to eq 'completed'
|
117
|
+
end
|
118
|
+
|
119
|
+
it 'restarts the worker if another worker was locked out during the run' do
|
120
|
+
expect {
|
121
|
+
process(@job) { SideJob.redis.set "#{@job.redis_key}:lock", Time.now.to_f }
|
122
|
+
}.to change {Sidekiq::Stats.new.enqueued}.by(1)
|
123
|
+
expect(@job.status).to eq 'queued'
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
describe 'prevents job loops' do
|
128
|
+
it 'does not run if called too many times in a minute' do
|
129
|
+
now = Time.now
|
130
|
+
allow(Time).to receive(:now) { now }
|
131
|
+
key = "#{@job.redis_key}:rate:#{Time.now.to_i/60}"
|
132
|
+
SideJob.redis.set key, SideJob::ServerMiddleware::CONFIGURATION[:max_runs_per_minute]
|
133
|
+
@run = false
|
134
|
+
process(@job) { @run = true }
|
135
|
+
expect(@run).to be false
|
136
|
+
expect(@job.status).to eq 'terminated'
|
137
|
+
end
|
138
|
+
|
139
|
+
it 'does run if not called too many times in a minute' do
|
140
|
+
now = Time.now
|
141
|
+
allow(Time).to receive(:now) { now }
|
142
|
+
key = "#{@job.redis_key}:rate:#{Time.now.to_i/60}"
|
143
|
+
SideJob.redis.set key, SideJob::ServerMiddleware::CONFIGURATION[:max_runs_per_minute]-1
|
144
|
+
@run = false
|
145
|
+
process(@job) { @run = true }
|
146
|
+
expect(@run).to be true
|
147
|
+
expect(@job.status).to eq 'completed'
|
148
|
+
end
|
149
|
+
|
150
|
+
it 'does not run if job is too deep' do
|
151
|
+
(SideJob::ServerMiddleware::CONFIGURATION[:max_depth]+1).times do |i|
|
152
|
+
@job = SideJob.queue(@queue, 'TestWorker', parent: @job, name: 'child')
|
153
|
+
end
|
154
|
+
@run = false
|
155
|
+
process(@job) { @run = true }
|
156
|
+
expect(@run).to be false
|
157
|
+
expect(@job.status).to eq 'terminated'
|
158
|
+
end
|
159
|
+
|
160
|
+
it 'does run if job is not too deep' do
|
161
|
+
SideJob::ServerMiddleware::CONFIGURATION[:max_depth].times do |i|
|
162
|
+
@job = SideJob.queue(@queue, 'TestWorker', parent: @job, name: 'child')
|
163
|
+
end
|
164
|
+
@run = false
|
165
|
+
process(@job) { @run = true }
|
166
|
+
expect(@run).to be true
|
167
|
+
expect(@job.status).to eq 'completed'
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
describe 'handles worker exceptions' do
|
172
|
+
it 'sets status to failed on exception and logs error' do
|
173
|
+
now = Time.now
|
174
|
+
allow(Time).to receive(:now) { now }
|
175
|
+
process(@job) { raise 'oops' }
|
176
|
+
expect(@job.status).to eq 'failed'
|
177
|
+
|
178
|
+
log = SideJob.logs.select {|log| log['error'] }
|
179
|
+
expect(log.size).to eq(1)
|
180
|
+
expect(log[0]['job']).to eq @job.id
|
181
|
+
expect(log[0]['error']).to eq('oops')
|
182
|
+
# check that we trim down backtrace to remove sidekiq lines
|
183
|
+
expect(log[0]['backtrace']).to_not match(/sidekiq/)
|
184
|
+
end
|
185
|
+
|
186
|
+
it 'does not set status to failed if status is not running' do
|
187
|
+
process(@job) do |worker|
|
188
|
+
worker.run
|
189
|
+
raise 'oops'
|
190
|
+
end
|
191
|
+
expect(@job.status).to eq 'queued'
|
192
|
+
end
|
193
|
+
|
194
|
+
it 'runs the parent job' do
|
195
|
+
@job.status = 'suspended'
|
196
|
+
child = SideJob.queue(@queue, 'TestWorker', parent: @job, name: 'child')
|
197
|
+
expect {
|
198
|
+
process(child) { raise 'oops' }
|
199
|
+
}.to change {Sidekiq::Stats.new.enqueued}.by(1)
|
200
|
+
@job.reload
|
201
|
+
expect(@job.status).to eq 'queued'
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
describe 'handles worker suspend' do
|
206
|
+
it 'sets status to suspended' do
|
207
|
+
process(@job) { |worker| worker.suspend }
|
208
|
+
expect(@job.status).to eq 'suspended'
|
209
|
+
end
|
210
|
+
|
211
|
+
it 'does not set status to suspended if job was requeued' do
|
212
|
+
process(@job) do |worker|
|
213
|
+
worker.run
|
214
|
+
worker.suspend
|
215
|
+
end
|
216
|
+
expect(@job.status).to eq 'queued'
|
217
|
+
end
|
218
|
+
end
|
219
|
+
|
220
|
+
describe 'handles job termination' do
|
221
|
+
it 'sets status to terminated upon run' do
|
222
|
+
@job.status = 'terminating'
|
223
|
+
process(@job) { raise 'should not be called' }
|
224
|
+
expect(@job.status).to eq 'terminated'
|
225
|
+
errors = SideJob.logs.select {|log| log['type'] == 'error'}
|
226
|
+
expect(errors.size).to eq 0
|
227
|
+
end
|
228
|
+
|
229
|
+
it 'runs parent' do
|
230
|
+
child = SideJob.queue(@queue, 'TestWorker', parent: @job, name: 'child')
|
231
|
+
child.status = 'terminating'
|
232
|
+
process(child) { raise 'should not be called' }
|
233
|
+
expect(child.status).to eq 'terminated'
|
234
|
+
expect(@job.status).to eq 'queued'
|
235
|
+
end
|
236
|
+
|
237
|
+
it 'calls worker shutdown method' do
|
238
|
+
@job = SideJob.queue(@queue, 'TestWorkerShutdown')
|
239
|
+
@job.status = 'terminating'
|
240
|
+
worker = process(@job) { raise 'not reached' }
|
241
|
+
expect(worker.shutdown_called).to be true
|
242
|
+
end
|
243
|
+
|
244
|
+
it 'logs but ignores exceptions thrown during shutdown' do
|
245
|
+
@job = SideJob.queue(@queue, 'TestWorkerShutdownError')
|
246
|
+
@job.status = 'terminating'
|
247
|
+
worker = process(@job) { raise 'not reached' }
|
248
|
+
logs = SideJob.logs.select {|log| log['error']}
|
249
|
+
expect(logs.size).to eq 1
|
250
|
+
expect(logs[0]['job']).to eq @job.id
|
251
|
+
expect(logs[0]['error']).to eq 'shutdown error'
|
252
|
+
end
|
253
|
+
end
|
254
|
+
end
|