sidejob 3.0.1 → 4.0.1
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 +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
|