sidejob 3.0.1 → 4.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile.lock +5 -5
- data/README.md +10 -14
- data/lib/sidejob.rb +29 -23
- data/lib/sidejob/job.rb +183 -213
- data/lib/sidejob/port.rb +112 -80
- data/lib/sidejob/server_middleware.rb +56 -50
- data/lib/sidejob/testing.rb +0 -2
- data/lib/sidejob/version.rb +1 -1
- data/lib/sidejob/worker.rb +28 -46
- data/spec/integration/fib_spec.rb +8 -4
- data/spec/integration/sum_spec.rb +0 -1
- data/spec/sidejob/job_spec.rb +323 -241
- data/spec/sidejob/port_spec.rb +152 -138
- data/spec/sidejob/server_middleware_spec.rb +27 -47
- data/spec/sidejob/worker_spec.rb +16 -84
- data/spec/sidejob_spec.rb +39 -16
- data/web/Gemfile +6 -0
- data/web/Gemfile.lock +43 -0
- data/web/app.rb +205 -0
- data/web/config.ru +14 -0
- metadata +6 -2
data/lib/sidejob/worker.rb
CHANGED
@@ -42,9 +42,9 @@ module SideJob
|
|
42
42
|
# Methods loaded last to override other included methods
|
43
43
|
module OverrideMethods
|
44
44
|
# Returns the jid set by sidekiq as the job id
|
45
|
-
# @return [
|
45
|
+
# @return [Integer] Job id
|
46
46
|
def id
|
47
|
-
jid
|
47
|
+
jid.to_i
|
48
48
|
end
|
49
49
|
end
|
50
50
|
|
@@ -58,12 +58,6 @@ module SideJob
|
|
58
58
|
base.extend(ClassMethods)
|
59
59
|
end
|
60
60
|
|
61
|
-
# Queues a child job, setting parent and by to self.
|
62
|
-
# @see SideJob.queue
|
63
|
-
def queue(queue, klass, **options)
|
64
|
-
SideJob.queue(queue, klass, options.merge({parent: self, by: "job:#{id}"}))
|
65
|
-
end
|
66
|
-
|
67
61
|
# Exception raised by {#suspend}
|
68
62
|
class Suspended < StandardError; end
|
69
63
|
|
@@ -79,55 +73,43 @@ module SideJob
|
|
79
73
|
# A worker should be idempotent (it can be called multiple times on the same state).
|
80
74
|
# Consider reading from a single port with a default value. Each time it is run, it could read the same data
|
81
75
|
# from the port. The output of the job then could depend on the number of times it is run.
|
82
|
-
#
|
76
|
+
# If all input port have defaults, this method remembers the call and will only yield once even over multiple runs.
|
77
|
+
# In addition, any writes to output ports inside the block will instead set the default value of the port.
|
83
78
|
# Yields data from the ports until either no ports have data or is suspended due to data on some but not all ports.
|
84
79
|
# @param inputs [Array<String>] List of input ports to read
|
85
80
|
# @yield [Array] Splat of input data in same order as inputs
|
86
81
|
# @raise [SideJob::Worker::Suspended] Raised if an input port without a default has data but not all ports
|
87
|
-
# @raise [RuntimeError] An error is raised if all input ports have default values
|
88
82
|
def for_inputs(*inputs, &block)
|
89
83
|
return unless inputs.length > 0
|
90
84
|
ports = inputs.map {|name| input(name)}
|
91
85
|
loop do
|
92
|
-
|
93
|
-
|
94
|
-
data = ports.map {|port| [ port.data?, port.default? ] }
|
95
|
-
raise "One of these input ports should not have a default value: #{inputs.join(',')}" if data.all? {|x| x[1]}
|
96
|
-
return unless data.any? {|x| x[0] && ! x[1] }
|
97
|
-
suspend unless data.all? {|x| x[0] }
|
86
|
+
SideJob::Port.log_group do
|
87
|
+
info = ports.map {|port| [ port.size > 0, port.default? ] }
|
98
88
|
|
99
|
-
|
89
|
+
return unless info.any? {|x| x[0] || x[1]} # Nothing to do if there's no data to read
|
90
|
+
if info.any? {|x| x[0]}
|
91
|
+
# some port has data, suspend unless every port has data or default
|
92
|
+
suspend unless info.all? {|x| x[0] || x[1] }
|
93
|
+
yield *ports.map(&:read)
|
94
|
+
elsif info.all? {|x| x[1]}
|
95
|
+
# all ports have default and no data
|
96
|
+
defaults = ports.map(&:default)
|
97
|
+
last_default = get(:for_inputs) || []
|
98
|
+
return unless defaults != last_default
|
99
|
+
set({for_inputs: defaults})
|
100
|
+
begin
|
101
|
+
Thread.current[:sidejob_port_write_default] = true
|
102
|
+
yield *defaults
|
103
|
+
ensure
|
104
|
+
Thread.current[:sidejob_port_write_default] = nil
|
105
|
+
end
|
106
|
+
return
|
107
|
+
else
|
108
|
+
# No ports have data and not every port has a default value so nothing to do
|
109
|
+
return
|
110
|
+
end
|
100
111
|
end
|
101
112
|
end
|
102
113
|
end
|
103
|
-
|
104
|
-
# Sets values in the job's internal state.
|
105
|
-
# @param data [Hash{String,Symbol => Object}] Data to update: objects should be JSON encodable
|
106
|
-
# @raise [RuntimeError] Error raised if job no longer exists
|
107
|
-
def set(data)
|
108
|
-
return unless data.size > 0
|
109
|
-
load_state
|
110
|
-
data.each_pair { |key, val| @state[key.to_s] = val }
|
111
|
-
save_state
|
112
|
-
end
|
113
|
-
|
114
|
-
# Unsets some fields in the job's internal state
|
115
|
-
# @param fields [Array<String,Symbol>] Fields to unset
|
116
|
-
# @raise [RuntimeError] Error raised if job no longer exists
|
117
|
-
def unset(*fields)
|
118
|
-
return unless fields.length > 0
|
119
|
-
load_state
|
120
|
-
fields.each { |field| @state.delete(field.to_s) }
|
121
|
-
save_state
|
122
|
-
end
|
123
|
-
|
124
|
-
private
|
125
|
-
|
126
|
-
def save_state
|
127
|
-
check_exists
|
128
|
-
if @state
|
129
|
-
SideJob.redis.hset 'jobs', id, @state.to_json
|
130
|
-
end
|
131
|
-
end
|
132
114
|
end
|
133
115
|
end
|
@@ -5,15 +5,20 @@ class TestFib
|
|
5
5
|
include SideJob::Worker
|
6
6
|
register(
|
7
7
|
inports: {
|
8
|
-
n: {
|
8
|
+
n: {}
|
9
9
|
},
|
10
10
|
outports: {
|
11
11
|
num: {}
|
12
12
|
}
|
13
13
|
)
|
14
14
|
def perform
|
15
|
-
|
16
|
-
|
15
|
+
if input(:n).data?
|
16
|
+
n = input(:n).read
|
17
|
+
else
|
18
|
+
n = get(:n)
|
19
|
+
end
|
20
|
+
suspend unless n
|
21
|
+
set({n: n})
|
17
22
|
|
18
23
|
if n <= 2
|
19
24
|
output(:num).write 1
|
@@ -44,7 +49,6 @@ describe TestFib do
|
|
44
49
|
job = SideJob.queue('testq', 'TestFib')
|
45
50
|
job.input(:n).write 6 # 1, 1, 2, 3, 5, 8
|
46
51
|
SideJob::Worker.drain_queue
|
47
|
-
job.reload
|
48
52
|
expect(job.status).to eq 'completed'
|
49
53
|
expect(job.output(:num).read).to eq(8)
|
50
54
|
end
|
data/spec/sidejob/job_spec.rb
CHANGED
@@ -1,25 +1,34 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
3
|
describe SideJob::Job do
|
4
|
+
describe '#initialize' do
|
5
|
+
it 'raises error if job does not exist' do
|
6
|
+
expect { SideJob::Job.new('123') }.to raise_error
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
4
10
|
describe '#==, #eql?' do
|
5
11
|
it 'two jobs with the same id are eq' do
|
6
|
-
|
7
|
-
expect(SideJob::Job.new(
|
12
|
+
@job = SideJob.queue('testq', 'TestWorker')
|
13
|
+
expect(SideJob::Job.new(@job.id)).to eq(@job)
|
14
|
+
expect(SideJob::Job.new(@job.id)).to eql(@job)
|
8
15
|
end
|
9
16
|
|
10
17
|
it 'two jobs with different id are not eq' do
|
11
|
-
|
12
|
-
|
18
|
+
@job = SideJob.queue('testq', 'TestWorker')
|
19
|
+
@job2 = SideJob.queue('testq', 'TestWorker')
|
20
|
+
expect(@job).not_to eq(@job2)
|
21
|
+
expect(@job).not_to eql(@job2)
|
13
22
|
end
|
14
23
|
end
|
15
24
|
|
16
25
|
describe '#hash' do
|
17
26
|
it 'uses hash of the job id and can be used as hash keys' do
|
18
|
-
job = SideJob
|
19
|
-
expect(job.hash).to eq(
|
27
|
+
job = SideJob.queue('testq', 'TestWorker')
|
28
|
+
expect(job.hash).to eq(job.id.hash)
|
20
29
|
h = {}
|
21
30
|
h[job] = 1
|
22
|
-
job2 = SideJob::Job.new(
|
31
|
+
job2 = SideJob::Job.new(job.id)
|
23
32
|
expect(job.hash).to eq(job2.hash)
|
24
33
|
h[job2] = 3
|
25
34
|
expect(h.keys.length).to be(1)
|
@@ -29,8 +38,8 @@ describe SideJob::Job do
|
|
29
38
|
|
30
39
|
describe '#to_s' do
|
31
40
|
it 'returns the redis key' do
|
32
|
-
job = SideJob
|
33
|
-
expect(job.to_s).to eq
|
41
|
+
job = SideJob.queue('testq', 'TestWorker')
|
42
|
+
expect(job.to_s).to eq "job:#{job.id}"
|
34
43
|
end
|
35
44
|
end
|
36
45
|
|
@@ -39,92 +48,12 @@ describe SideJob::Job do
|
|
39
48
|
@job = SideJob.queue('testq', 'TestWorker')
|
40
49
|
expect(@job.exists?).to be true
|
41
50
|
end
|
42
|
-
it 'returns false if job does not exist' do
|
43
|
-
expect(SideJob::Job.new('job').exists?).to be false
|
44
|
-
end
|
45
|
-
end
|
46
51
|
|
47
|
-
|
48
|
-
before do
|
52
|
+
it 'returns false if job no longer exists' do
|
49
53
|
@job = SideJob.queue('testq', 'TestWorker')
|
50
|
-
@
|
51
|
-
@
|
52
|
-
|
53
|
-
|
54
|
-
it 'logs to SideJob.log if logger not set' do
|
55
|
-
expect(SideJob).to receive(:log).with @entry_job
|
56
|
-
@job.log(@entry)
|
57
|
-
end
|
58
|
-
|
59
|
-
it 'logs to job logger if set' do
|
60
|
-
expect(SideJob).not_to receive(:log).with @entry_job
|
61
|
-
@job.logger = Class.new
|
62
|
-
expect(@job.logger).to receive(:log).with @entry_job
|
63
|
-
@job.log(@entry)
|
64
|
-
end
|
65
|
-
end
|
66
|
-
|
67
|
-
describe '#group_port_logs' do
|
68
|
-
before do
|
69
|
-
@job = SideJob.queue('testq', 'TestWorker', inports: {port1: {}})
|
70
|
-
@port = @job.input(:port1)
|
71
|
-
now = Time.now
|
72
|
-
allow(Time).to receive(:now) { now }
|
73
|
-
end
|
74
|
-
|
75
|
-
it 'does not generate a log if no logs occur' do
|
76
|
-
@job.group_port_logs {}
|
77
|
-
expect(SideJob.logs.length).to eq 0
|
78
|
-
end
|
79
|
-
|
80
|
-
it 'passes through logs not generated by port read/write' do
|
81
|
-
@job.group_port_logs do
|
82
|
-
@job.log({'test' => 123})
|
83
|
-
expect(SideJob.logs).to eq [{'timestamp' => SideJob.timestamp, 'test' => 123, 'job' => @job.id}]
|
84
|
-
end
|
85
|
-
end
|
86
|
-
|
87
|
-
it 'merges logs from port reads and writes' do
|
88
|
-
@job.group_port_logs do
|
89
|
-
@port.write 'hello'
|
90
|
-
@port.write 2
|
91
|
-
end
|
92
|
-
expect(SideJob.logs).to eq [{'timestamp' => SideJob.timestamp, 'job' => @job.id,
|
93
|
-
'read' => [],
|
94
|
-
'write' => [{'job' => @job.id, 'inport' => 'port1', 'data' => ['hello', 2]}]}]
|
95
|
-
end
|
96
|
-
|
97
|
-
it 'does not write out a log entry until the end of outermost group' do
|
98
|
-
@job.group_port_logs do
|
99
|
-
@port.write 'hello'
|
100
|
-
@job.group_port_logs do
|
101
|
-
@port.write 2
|
102
|
-
end
|
103
|
-
expect(SideJob.logs.length).to eq 0
|
104
|
-
end
|
105
|
-
expect(SideJob.logs).to eq [{'timestamp' => SideJob.timestamp, 'job' => @job.id,
|
106
|
-
'read' => [],
|
107
|
-
'write' => [{'job' => @job.id, 'inport' => 'port1', 'data' => ['hello', 2]}]}]
|
108
|
-
end
|
109
|
-
|
110
|
-
it 'can overwrite job for log attribution' do
|
111
|
-
@job.group_port_logs(job: 1234) do
|
112
|
-
@port.write 'hello'
|
113
|
-
@port.read
|
114
|
-
end
|
115
|
-
expect(SideJob.logs).to eq [{'timestamp' => SideJob.timestamp, 'job' => 1234,
|
116
|
-
'read' => [{'job' => @job.id, 'inport' => 'port1', 'data' => ['hello']}],
|
117
|
-
'write' => [{'job' => @job.id, 'inport' => 'port1', 'data' => ['hello']}]}]
|
118
|
-
end
|
119
|
-
|
120
|
-
it 'can include arbitrary metadata' do
|
121
|
-
@job.group_port_logs(user: 'test') do
|
122
|
-
@port.write 'hello'
|
123
|
-
@port.read
|
124
|
-
end
|
125
|
-
expect(SideJob.logs).to eq [{'timestamp' => SideJob.timestamp, 'job' => @job.id, 'user' => 'test',
|
126
|
-
'read' => [{'job' => @job.id, 'inport' => 'port1', 'data' => ['hello']}],
|
127
|
-
'write' => [{'job' => @job.id, 'inport' => 'port1', 'data' => ['hello']}]}]
|
54
|
+
@job.status = 'terminated'
|
55
|
+
@job.delete
|
56
|
+
expect(@job.exists?).to be false
|
128
57
|
end
|
129
58
|
end
|
130
59
|
|
@@ -143,70 +72,28 @@ describe SideJob::Job do
|
|
143
72
|
end
|
144
73
|
end
|
145
74
|
|
146
|
-
describe '#terminate' do
|
147
|
-
before do
|
148
|
-
@job = SideJob.queue('testq', 'TestWorker')
|
149
|
-
end
|
150
|
-
|
151
|
-
it 'sets the status to terminating' do
|
152
|
-
@job.terminate
|
153
|
-
expect(@job.status).to eq 'terminating'
|
154
|
-
end
|
155
|
-
|
156
|
-
it 'does nothing if status is terminated' do
|
157
|
-
@job.status = 'terminated'
|
158
|
-
@job.terminate
|
159
|
-
expect(@job.status).to eq 'terminated'
|
160
|
-
end
|
161
|
-
|
162
|
-
it 'throws error and immediately sets status to terminated if job class is unregistered' do
|
163
|
-
SideJob.redis.del 'workers:testq'
|
164
|
-
expect { @job.terminate }.to raise_error
|
165
|
-
expect(@job.status).to eq 'terminated'
|
166
|
-
end
|
167
|
-
|
168
|
-
it 'queues the job for termination run' do
|
169
|
-
expect {
|
170
|
-
@job.terminate
|
171
|
-
}.to change {Sidekiq::Stats.new.enqueued}.by(1)
|
172
|
-
end
|
173
|
-
|
174
|
-
it 'by default does not terminate children' do
|
175
|
-
child = SideJob.queue('testq', 'TestWorker', parent: @job, name: 'child')
|
176
|
-
expect(child.status).to eq 'queued'
|
177
|
-
@job.terminate
|
178
|
-
expect(child.status).to eq 'queued'
|
179
|
-
end
|
180
|
-
|
181
|
-
it 'can recursively terminate' do
|
182
|
-
5.times {|i| SideJob.queue('testq', 'TestWorker', parent: @job, name: "child#{i}") }
|
183
|
-
@job.terminate(recursive: true)
|
184
|
-
@job.children.each_value do |child|
|
185
|
-
expect(child.status).to eq 'terminating'
|
186
|
-
end
|
187
|
-
end
|
188
|
-
end
|
189
|
-
|
190
75
|
describe '#run' do
|
191
76
|
before do
|
192
77
|
@job = SideJob.queue('testq', 'TestWorker')
|
78
|
+
Sidekiq::Queue.new('testq').find_job(@job.id).delete
|
79
|
+
@job.status = 'completed'
|
193
80
|
end
|
194
81
|
|
195
|
-
%w{queued running suspended completed
|
82
|
+
%w{queued running suspended completed}.each do |status|
|
196
83
|
it "queues the job if status is #{status}" do
|
197
84
|
expect {
|
198
85
|
@job.status = status
|
199
|
-
@job.run
|
86
|
+
expect(@job.run).to eq @job
|
200
87
|
expect(@job.status).to eq 'queued'
|
201
88
|
}.to change {Sidekiq::Stats.new.enqueued}.by(1)
|
202
89
|
end
|
203
90
|
end
|
204
91
|
|
205
|
-
%w{terminating terminated}.each do |status|
|
92
|
+
%w{failed terminating terminated}.each do |status|
|
206
93
|
it "does not queue the job if status is #{status}" do
|
207
94
|
expect {
|
208
95
|
@job.status = status
|
209
|
-
@job.run
|
96
|
+
expect(@job.run).to be nil
|
210
97
|
expect(@job.status).to eq status
|
211
98
|
}.to change {Sidekiq::Stats.new.enqueued}.by(0)
|
212
99
|
end
|
@@ -214,12 +101,37 @@ describe SideJob::Job do
|
|
214
101
|
it "queues the job if status is #{status} and force=true" do
|
215
102
|
expect {
|
216
103
|
@job.status = status
|
217
|
-
@job.run(force: true)
|
104
|
+
expect(@job.run(force: true)).to eq @job
|
218
105
|
expect(@job.status).to eq 'queued'
|
219
106
|
}.to change {Sidekiq::Stats.new.enqueued}.by(1)
|
220
107
|
end
|
221
108
|
end
|
222
109
|
|
110
|
+
it "does not queue the job if it is already queued" do
|
111
|
+
@job.run
|
112
|
+
expect {
|
113
|
+
expect(@job.run).to eq @job
|
114
|
+
}.to change {Sidekiq::Stats.new.enqueued}.by(0)
|
115
|
+
end
|
116
|
+
|
117
|
+
it 'does nothing if no parent job and parent=true' do
|
118
|
+
@job.status = 'completed'
|
119
|
+
expect {
|
120
|
+
expect(@job.run(parent: true)).to be nil
|
121
|
+
expect(@job.status).to eq 'completed'
|
122
|
+
}.to change {Sidekiq::Stats.new.enqueued}.by(0)
|
123
|
+
end
|
124
|
+
|
125
|
+
it 'runs parent job if parent=true' do
|
126
|
+
parent = SideJob.queue('testq', 'TestWorker')
|
127
|
+
parent.adopt(@job, 'child')
|
128
|
+
@job.status = 'completed'
|
129
|
+
parent.status = 'completed'
|
130
|
+
expect(@job.run(parent: true)).to eq parent
|
131
|
+
expect(@job.status).to eq 'completed'
|
132
|
+
expect(parent.status).to eq 'queued'
|
133
|
+
end
|
134
|
+
|
223
135
|
it 'throws error and immediately sets status to terminated if job class is unregistered' do
|
224
136
|
SideJob.redis.del "workers:#{@job.get(:queue)}"
|
225
137
|
expect { @job.run }.to raise_error
|
@@ -256,6 +168,100 @@ describe SideJob::Job do
|
|
256
168
|
end
|
257
169
|
end
|
258
170
|
|
171
|
+
describe '#terminated?' do
|
172
|
+
before do
|
173
|
+
@job = SideJob.queue('testq', 'TestWorker')
|
174
|
+
end
|
175
|
+
|
176
|
+
it 'returns false if job status is not terminated' do
|
177
|
+
expect(@job.terminated?).to be false
|
178
|
+
end
|
179
|
+
|
180
|
+
it 'returns true if job status is terminated' do
|
181
|
+
@job.status = 'terminated'
|
182
|
+
expect(@job.terminated?).to be true
|
183
|
+
end
|
184
|
+
|
185
|
+
it 'returns false if child job is not terminated' do
|
186
|
+
@job.status = 'terminated'
|
187
|
+
SideJob.queue('testq', 'TestWorker', parent: @job, name: 'child')
|
188
|
+
expect(@job.terminated?).to be false
|
189
|
+
end
|
190
|
+
|
191
|
+
it 'returns true if child job is terminated' do
|
192
|
+
@job.status = 'terminated'
|
193
|
+
child = SideJob.queue('testq', 'TestWorker', parent: @job, name: 'child')
|
194
|
+
child.status = 'terminated'
|
195
|
+
expect(@job.terminated?).to be true
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
describe '#terminate' do
|
200
|
+
before do
|
201
|
+
@job = SideJob.queue('testq', 'TestWorker')
|
202
|
+
Sidekiq::Queue.new('testq').find_job(@job.id).delete
|
203
|
+
@job.status = 'completed'
|
204
|
+
end
|
205
|
+
|
206
|
+
it 'sets the status to terminating' do
|
207
|
+
@job.terminate
|
208
|
+
expect(@job.status).to eq 'terminating'
|
209
|
+
end
|
210
|
+
|
211
|
+
it 'does nothing if status is terminated' do
|
212
|
+
@job.status = 'terminated'
|
213
|
+
@job.terminate
|
214
|
+
expect(@job.status).to eq 'terminated'
|
215
|
+
end
|
216
|
+
|
217
|
+
it 'throws error and immediately sets status to terminated if job class is unregistered' do
|
218
|
+
SideJob.redis.del 'workers:testq'
|
219
|
+
expect { @job.terminate }.to raise_error
|
220
|
+
expect(@job.status).to eq 'terminated'
|
221
|
+
end
|
222
|
+
|
223
|
+
it 'queues the job for termination run' do
|
224
|
+
expect {
|
225
|
+
@job.terminate
|
226
|
+
}.to change {Sidekiq::Stats.new.enqueued}.by(1)
|
227
|
+
end
|
228
|
+
|
229
|
+
it 'by default does not terminate children' do
|
230
|
+
child = SideJob.queue('testq', 'TestWorker', parent: @job, name: 'child')
|
231
|
+
expect(child.status).to eq 'queued'
|
232
|
+
@job.terminate
|
233
|
+
expect(child.status).to eq 'queued'
|
234
|
+
end
|
235
|
+
|
236
|
+
it 'can recursively terminate' do
|
237
|
+
5.times {|i| SideJob.queue('testq', 'TestWorker', parent: @job, name: "child#{i}") }
|
238
|
+
@job.terminate(recursive: true)
|
239
|
+
@job.children.each_value do |child|
|
240
|
+
expect(child.status).to eq 'terminating'
|
241
|
+
end
|
242
|
+
end
|
243
|
+
end
|
244
|
+
|
245
|
+
describe '#queue' do
|
246
|
+
before do
|
247
|
+
@job = SideJob.queue('testq', 'TestWorker')
|
248
|
+
end
|
249
|
+
|
250
|
+
it 'can queue child jobs' do
|
251
|
+
expect(SideJob).to receive(:queue).with('testq', 'TestWorker', args: [1,2], inports: {'myport' => {}}, parent: @job, name: 'child', by: "job:#{@job.id}").and_call_original
|
252
|
+
expect {
|
253
|
+
child = @job.queue('testq', 'TestWorker', args: [1,2], inports: {'myport' => {}}, name: 'child')
|
254
|
+
expect(child.parent).to eq(@job)
|
255
|
+
expect(@job.children).to eq('child' => child)
|
256
|
+
}.to change {Sidekiq::Stats.new.enqueued}.by(1)
|
257
|
+
end
|
258
|
+
|
259
|
+
it 'queues with by string set to self' do
|
260
|
+
child = @job.queue('testq', 'TestWorker', name: 'child')
|
261
|
+
expect(child.get(:created_by)).to eq "job:#{@job.id}"
|
262
|
+
end
|
263
|
+
end
|
264
|
+
|
259
265
|
describe '#child' do
|
260
266
|
it 'returns nil for missing child' do
|
261
267
|
job = SideJob.queue('testq', 'TestWorker')
|
@@ -278,46 +284,74 @@ describe SideJob::Job do
|
|
278
284
|
end
|
279
285
|
end
|
280
286
|
|
281
|
-
describe '#
|
282
|
-
it '
|
287
|
+
describe '#disown' do
|
288
|
+
it 'raises error if child cannot be found' do
|
289
|
+
parent = SideJob.queue('testq', 'TestWorker')
|
290
|
+
expect { parent.disown('child') }.to raise_error
|
291
|
+
end
|
292
|
+
|
293
|
+
it 'raises error if job is not child' do
|
294
|
+
parent = SideJob.queue('testq', 'TestWorker')
|
283
295
|
job = SideJob.queue('testq', 'TestWorker')
|
284
|
-
expect(job
|
296
|
+
expect { parent.disown(job) }.to raise_error
|
285
297
|
end
|
286
298
|
|
287
|
-
it '
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
299
|
+
it 'disassociates a child job from the parent' do
|
300
|
+
parent = SideJob.queue('testq', 'TestWorker')
|
301
|
+
child = SideJob.queue('testq', 'TestWorker', parent: parent, name: 'child')
|
302
|
+
expect(child.parent).to eq(parent)
|
303
|
+
expect(parent.child('child')).to eq child
|
304
|
+
parent.disown('child')
|
305
|
+
expect(child.parent).to be nil
|
306
|
+
expect(parent.child('child')).to be nil
|
307
|
+
end
|
308
|
+
|
309
|
+
it 'can specify a job' do
|
310
|
+
parent = SideJob.queue('testq', 'TestWorker')
|
311
|
+
child = SideJob.queue('testq', 'TestWorker', parent: parent, name: 'child')
|
312
|
+
expect(child.parent).to eq(parent)
|
313
|
+
expect(parent.child('child')).to eq child
|
314
|
+
parent.disown(child)
|
315
|
+
expect(child.parent).to be nil
|
316
|
+
expect(parent.child('child')).to be nil
|
293
317
|
end
|
294
318
|
end
|
295
319
|
|
296
|
-
describe '#
|
297
|
-
|
298
|
-
|
320
|
+
describe '#adopt' do
|
321
|
+
it 'can adopt an orphan job' do
|
322
|
+
job = SideJob.queue('testq', 'TestWorker')
|
323
|
+
child = SideJob.queue('testq', 'TestWorker')
|
324
|
+
expect(child.parent).to be nil
|
325
|
+
job.adopt(child, 'child')
|
326
|
+
expect(child.parent).to eq(job)
|
299
327
|
end
|
300
328
|
|
301
|
-
it '
|
302
|
-
|
329
|
+
it 'raises error when adopting self' do
|
330
|
+
job = SideJob.queue('testq', 'TestWorker')
|
331
|
+
expect(job.parent).to be nil
|
332
|
+
expect { job.adopt(job, 'self') }.to raise_error
|
303
333
|
end
|
304
334
|
|
305
|
-
it '
|
306
|
-
|
307
|
-
|
335
|
+
it 'raises error if job already has a parent' do
|
336
|
+
job = SideJob.queue('testq', 'TestWorker')
|
337
|
+
job2 = SideJob.queue('testq', 'TestWorker')
|
338
|
+
child = SideJob.queue('testq', 'TestWorker')
|
339
|
+
job.adopt(child, 'child')
|
340
|
+
expect { job2.adopt(child, 'mine') }.to raise_error
|
308
341
|
end
|
309
342
|
|
310
|
-
it '
|
311
|
-
|
312
|
-
SideJob.queue('testq', 'TestWorker'
|
313
|
-
expect
|
343
|
+
it 'raises error if no name is given' do
|
344
|
+
job = SideJob.queue('testq', 'TestWorker')
|
345
|
+
child = SideJob.queue('testq', 'TestWorker')
|
346
|
+
expect { job.adopt(child, nil) }.to raise_error
|
314
347
|
end
|
315
348
|
|
316
|
-
it '
|
317
|
-
|
318
|
-
child = SideJob.queue('testq', 'TestWorker'
|
319
|
-
|
320
|
-
|
349
|
+
it 'raises error if name is not unique' do
|
350
|
+
job = SideJob.queue('testq', 'TestWorker')
|
351
|
+
child = SideJob.queue('testq', 'TestWorker')
|
352
|
+
child2 = SideJob.queue('testq', 'TestWorker')
|
353
|
+
job.adopt(child, 'child')
|
354
|
+
expect { job.adopt(child2, 'child') }.to raise_error
|
321
355
|
end
|
322
356
|
end
|
323
357
|
|
@@ -359,17 +393,24 @@ describe SideJob::Job do
|
|
359
393
|
@job.delete
|
360
394
|
expect(SideJob.redis {|redis| redis.keys('job:*').length}).to be(0)
|
361
395
|
end
|
396
|
+
|
397
|
+
it 'disowns job from parent' do
|
398
|
+
child = SideJob.queue('testq', 'TestWorker', parent: @job, name: 'child')
|
399
|
+
expect(@job.children.size).to eq 1
|
400
|
+
child.status = 'terminated'
|
401
|
+
child.delete
|
402
|
+
expect(@job.children.size).to eq 0
|
403
|
+
end
|
362
404
|
end
|
363
405
|
|
364
406
|
# Tests are identical for input and output port methods
|
365
407
|
%i{in out}.each do |type|
|
366
408
|
describe "##{type}put" do
|
367
|
-
it "returns a
|
409
|
+
it "returns a #{type}put port" do
|
368
410
|
spec = {}
|
369
411
|
spec[:"#{type}ports"] = {port: {}}
|
370
412
|
@job = SideJob.queue('testq', 'TestWorker', **spec)
|
371
413
|
expect(@job.send("#{type}put", :port)).to eq(SideJob::Port.new(@job, type, :port))
|
372
|
-
expect(@job.send("#{type}put", :port)).to be(@job.send("#{type}put", :port))
|
373
414
|
end
|
374
415
|
|
375
416
|
it 'raises error on unknown port' do
|
@@ -379,12 +420,11 @@ describe SideJob::Job do
|
|
379
420
|
|
380
421
|
it 'can dynamically create ports' do
|
381
422
|
spec = {}
|
382
|
-
spec[:"#{type}ports"] = {'*' => {
|
423
|
+
spec[:"#{type}ports"] = {'*' => {default: 123}}
|
383
424
|
@job = SideJob.queue('testq', 'TestWorker', **spec)
|
384
425
|
expect(@job.send("#{type}ports").size).to eq 0
|
385
426
|
port = @job.send("#{type}put", :newport)
|
386
427
|
expect(@job.send("#{type}ports").size).to eq 1
|
387
|
-
expect(port.mode).to eq :memory
|
388
428
|
expect(port.default).to eq 123
|
389
429
|
end
|
390
430
|
end
|
@@ -403,29 +443,32 @@ describe SideJob::Job do
|
|
403
443
|
|
404
444
|
it 'can specify ports with options' do
|
405
445
|
expect(@job.send("#{type}ports").size).to eq 0
|
406
|
-
@job.send("#{type}ports=", {myport: {
|
446
|
+
@job.send("#{type}ports=", {myport: {default: 'def'}})
|
407
447
|
expect(@job.send("#{type}ports").size).to eq 1
|
408
448
|
expect(@job.send("#{type}ports").map(&:name)).to include(:myport)
|
409
|
-
expect(@job.send("#{type}put", :myport).mode).to eq :memory
|
410
449
|
expect(@job.send("#{type}put", :myport).default).to eq 'def'
|
411
450
|
end
|
412
451
|
|
452
|
+
it 'does not modify passed in port options' do
|
453
|
+
ports = {myport: {default: [1,2]}}.freeze
|
454
|
+
expect { @job.send("#{type}ports=", ports) }.to_not raise_error
|
455
|
+
end
|
456
|
+
|
413
457
|
it 'merges ports with the worker configuration' do
|
414
|
-
allow(
|
415
|
-
@job.send("#{type}ports=", {port2: {
|
416
|
-
expect(@job.send("#{type}ports").size).to eq
|
417
|
-
expect(@job.send("#{type}
|
458
|
+
allow(SideJob::Worker).to receive(:config) { {"#{type}ports" => {'port1' => {}, 'port2' => {default: 'worker'}}}}
|
459
|
+
@job.send("#{type}ports=", {port2: {default: 'new'}})
|
460
|
+
expect(@job.send("#{type}ports").size).to eq 2
|
461
|
+
expect(@job.send("#{type}put", :port2).default).to eq 'new'
|
418
462
|
end
|
419
463
|
|
420
|
-
it 'can change existing port
|
421
|
-
@job.send("#{type}ports=", {myport: {}})
|
464
|
+
it 'can change existing port default while keeping data intact' do
|
465
|
+
@job.send("#{type}ports=", {myport: {default: 'orig'}})
|
422
466
|
@job.send("#{type}put", :myport).write 'data'
|
423
|
-
@job.send("#{type}ports=", {myport: {
|
467
|
+
@job.send("#{type}ports=", {myport: {default: 'new'}})
|
424
468
|
expect(@job.send("#{type}ports").size).to eq 1
|
425
|
-
expect(@job.send("#{type}put", :myport).
|
426
|
-
expect(@job.send("#{type}put", :myport).default).to eq 'def'
|
469
|
+
expect(@job.send("#{type}put", :myport).default).to eq 'new'
|
427
470
|
expect(@job.send("#{type}put", :myport).read).to eq 'data'
|
428
|
-
expect(@job.send("#{type}put", :myport).read).to eq '
|
471
|
+
expect(@job.send("#{type}put", :myport).read).to eq 'new'
|
429
472
|
end
|
430
473
|
|
431
474
|
it 'deletes no longer used ports' do
|
@@ -435,45 +478,26 @@ describe SideJob::Job do
|
|
435
478
|
expect(@job.send("#{type}ports").map(&:name)).not_to include(:myport)
|
436
479
|
expect { @job.send("#{type}put", :myport) }.to raise_error
|
437
480
|
end
|
481
|
+
end
|
482
|
+
end
|
438
483
|
|
439
|
-
|
440
|
-
|
441
|
-
|
442
|
-
|
443
|
-
|
444
|
-
|
445
|
-
it 'can clear port data' do
|
446
|
-
@job.send("#{type}ports=", {myport: {}})
|
447
|
-
@job.send("#{type}put", :myport).write 'data'
|
448
|
-
expect(@job.send("#{type}put", :myport).size).to eq 1
|
449
|
-
@job.send("#{type}ports=", {myport: {data: []}})
|
450
|
-
expect(@job.send("#{type}put", :myport).size).to eq 0
|
451
|
-
end
|
452
|
-
|
453
|
-
it 'overwrites existing data' do
|
454
|
-
@job.send("#{type}ports=", {myport: {}})
|
455
|
-
@job.send("#{type}put", :myport).write 'data'
|
456
|
-
expect(@job.send("#{type}put", :myport).size).to eq 1
|
457
|
-
@job.send("#{type}ports=", {myport: {data: [1,'abc']}})
|
458
|
-
expect(@job.send("#{type}put", :myport).size).to eq 2
|
459
|
-
expect(@job.send("#{type}put", :myport).read).to eq 1
|
460
|
-
expect(@job.send("#{type}put", :myport).read).to eq 'abc'
|
461
|
-
end
|
484
|
+
describe '#state' do
|
485
|
+
before do
|
486
|
+
now = Time.now
|
487
|
+
allow(Time).to receive(:now) { now }
|
488
|
+
@job = SideJob.queue('testq', 'TestWorker')
|
489
|
+
end
|
462
490
|
|
463
|
-
|
464
|
-
|
465
|
-
|
466
|
-
@job.send("#{type}ports=", {myport: {data: [1,'abc']}})
|
467
|
-
expect(SideJob.logs).to eq [{'timestamp' => SideJob.timestamp, 'job' => @job.id, 'read' => [], 'write' => [{'job' => @job.id, "#{type}port" => 'myport', 'data' => [1,'abc']}]}]
|
468
|
-
end
|
491
|
+
it 'returns job state with common and internal keys' do
|
492
|
+
@job.set({abc: 123})
|
493
|
+
expect(@job.state).to eq({"queue"=>"testq", "class"=>"TestWorker", "args"=>nil, "created_by"=>nil, "created_at"=>SideJob.timestamp, 'status' => 'queued', 'abc' => 123})
|
469
494
|
end
|
470
495
|
end
|
471
496
|
|
472
497
|
describe '#get' do
|
473
498
|
before do
|
474
499
|
@job = SideJob.queue('testq', 'TestWorker')
|
475
|
-
|
476
|
-
@job.reload
|
500
|
+
@job.set({field1: 'value1', field2: [1,2], field3: 123 })
|
477
501
|
end
|
478
502
|
|
479
503
|
it 'returns a value from job state using symbol key' do
|
@@ -492,52 +516,110 @@ describe SideJob::Job do
|
|
492
516
|
expect(@job.get(:field2)).to eq [1, 2]
|
493
517
|
end
|
494
518
|
|
495
|
-
it '
|
519
|
+
it 'always returns the latest value' do
|
496
520
|
expect(@job.get(:field3)).to eq 123
|
497
521
|
SideJob.redis.hmset @job.redis_key, :field3, '789'
|
498
|
-
expect(@job.get(:field3)).to eq
|
522
|
+
expect(@job.get(:field3)).to eq 789
|
499
523
|
end
|
524
|
+
end
|
500
525
|
|
501
|
-
|
502
|
-
|
503
|
-
|
504
|
-
job2.status = 'terminated'
|
505
|
-
job2.delete
|
506
|
-
expect { @job.get(:key) }.to raise_error
|
526
|
+
describe '#set' do
|
527
|
+
before do
|
528
|
+
@job = SideJob.queue('testq', 'TestWorker')
|
507
529
|
end
|
508
530
|
|
509
|
-
it '
|
510
|
-
@job.
|
511
|
-
|
512
|
-
|
513
|
-
|
514
|
-
|
531
|
+
it 'can save state in redis' do
|
532
|
+
@job.set(test: 'data', test2: 123)
|
533
|
+
state = @job.state
|
534
|
+
expect(state['test']).to eq 'data'
|
535
|
+
expect(state['test2']).to eq 123
|
536
|
+
|
537
|
+
# test updating
|
538
|
+
@job.set(test: 'data2')
|
539
|
+
expect(@job.get('test')).to eq 'data2'
|
540
|
+
end
|
541
|
+
|
542
|
+
it 'can update values' do
|
543
|
+
3.times do |i|
|
544
|
+
@job.set key: [i]
|
545
|
+
expect(@job.get(:key)).to eq [i]
|
546
|
+
expect(JSON.parse(SideJob.redis.hget(@job.redis_key, 'key'))).to eq [i]
|
547
|
+
end
|
548
|
+
end
|
549
|
+
|
550
|
+
it 'raises error if job no longer exists' do
|
551
|
+
@job.status = 'terminated'
|
552
|
+
SideJob.find(@job.id).delete
|
553
|
+
expect { @job.set key: 123 }.to raise_error
|
515
554
|
end
|
516
555
|
end
|
517
556
|
|
518
|
-
describe '#
|
557
|
+
describe '#unset' do
|
519
558
|
before do
|
520
559
|
@job = SideJob.queue('testq', 'TestWorker')
|
521
560
|
end
|
522
561
|
|
523
|
-
it '
|
524
|
-
|
525
|
-
|
526
|
-
@job.
|
527
|
-
expect(@job.get(:
|
562
|
+
it 'unsets fields' do
|
563
|
+
@job.set(a: 123, b: 456, c: 789)
|
564
|
+
@job.unset('a', :b)
|
565
|
+
expect(@job.get(:a)).to eq nil
|
566
|
+
expect(@job.get(:b)).to eq nil
|
567
|
+
expect(@job.get(:c)).to eq 789
|
528
568
|
end
|
529
569
|
end
|
530
570
|
|
531
|
-
describe '#
|
571
|
+
describe '#lock' do
|
532
572
|
before do
|
533
573
|
@job = SideJob.queue('testq', 'TestWorker')
|
534
574
|
end
|
535
575
|
|
536
|
-
it '
|
537
|
-
|
538
|
-
@job.
|
539
|
-
|
540
|
-
|
576
|
+
it 'sets the ttl on the lock expiration' do
|
577
|
+
@job.lock(123)
|
578
|
+
expect(SideJob.redis.ttl("#{@job.redis_key}:lock")).to be_within(1).of(123)
|
579
|
+
end
|
580
|
+
|
581
|
+
it 'returns a random token when lock is acquired' do
|
582
|
+
allow(SecureRandom).to receive(:uuid) { 'abcde' }
|
583
|
+
expect(SideJob.redis.exists("#{@job.redis_key}:lock")).to be false
|
584
|
+
expect(@job.lock(100)).to eq('abcde')
|
585
|
+
expect(SideJob.redis.get("#{@job.redis_key}:lock")).to eq 'abcde'
|
586
|
+
end
|
587
|
+
|
588
|
+
it 'returns nil if job is locked' do
|
589
|
+
expect(SideJob.redis.exists("#{@job.redis_key}:lock")).to be false
|
590
|
+
expect(@job.lock(100)).to_not be nil
|
591
|
+
expect(@job.lock(100, retries: 1)).to be nil
|
592
|
+
end
|
593
|
+
end
|
594
|
+
|
595
|
+
describe '#refresh_lock' do
|
596
|
+
before do
|
597
|
+
@job = SideJob.queue('testq', 'TestWorker')
|
598
|
+
end
|
599
|
+
|
600
|
+
it 'resets the lock expiration' do
|
601
|
+
@job.lock(100)
|
602
|
+
expect(SideJob.redis.ttl("#{@job.redis_key}:lock")).to be_within(1).of(100)
|
603
|
+
@job.refresh_lock(200)
|
604
|
+
expect(SideJob.redis.ttl("#{@job.redis_key}:lock")).to be_within(1).of(200)
|
605
|
+
end
|
606
|
+
end
|
607
|
+
|
608
|
+
describe '#unlock' do
|
609
|
+
before do
|
610
|
+
@job = SideJob.queue('testq', 'TestWorker')
|
611
|
+
end
|
612
|
+
|
613
|
+
it 'unlocks when the token matches' do
|
614
|
+
token = @job.lock(100)
|
615
|
+
expect(@job.unlock(token)).to be true
|
616
|
+
expect(SideJob.redis.exists("#{@job.redis_key}:lock")).to be false
|
617
|
+
end
|
618
|
+
|
619
|
+
it 'does not unlock when the token does not match' do
|
620
|
+
token = @job.lock(100)
|
621
|
+
expect(@job.unlock("#{token}x")).to be false
|
622
|
+
expect(SideJob.redis.exists("#{@job.redis_key}:lock")).to be true
|
541
623
|
end
|
542
624
|
end
|
543
625
|
end
|