sidejob 3.0.1 → 4.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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 [String] Job id
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
- # To prevent this, this method requires that there be at least one input port which does not have a default.
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
- group_port_logs(job: id) do
93
- # error if ports all have defaults, complete if no non-default port inputs, suspend if partial inputs
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
- yield *ports.map(&:read)
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: {mode: :memory}
8
+ n: {}
9
9
  },
10
10
  outports: {
11
11
  num: {}
12
12
  }
13
13
  )
14
14
  def perform
15
- suspend unless input(:n).data?
16
- n = input(:n).read
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
@@ -32,7 +32,6 @@ describe TestSumFlow do
32
32
  it 'calls child job to sum numbers' do
33
33
  job = SideJob.queue('testq', 'TestSumFlow')
34
34
  SideJob::Worker.drain_queue
35
- job.reload
36
35
  expect(job.status).to eq 'completed'
37
36
  expect(job.output(:out).read).to eq(11)
38
37
  end
@@ -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
- expect(SideJob::Job.new('123')).to eq(SideJob::Job.new('123'))
7
- expect(SideJob::Job.new('123')).to eql(SideJob::Job.new('123'))
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
- expect(SideJob::Job.new('123')).not_to eq(SideJob::Job.new('456'))
12
- expect(SideJob::Job.new('123')).not_to eql(SideJob::Job.new('456'))
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::Job.new('abc')
19
- expect(job.hash).to eq('abc'.hash)
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('abc')
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::Job.new('abc')
33
- expect(job.to_s).to eq 'job:abc'
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
- describe '#log' do
48
- before do
52
+ it 'returns false if job no longer exists' do
49
53
  @job = SideJob.queue('testq', 'TestWorker')
50
- @entry = {'abc' => [1,2]}
51
- @entry_job = @entry.merge({job: @job.id})
52
- end
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 failed}.each do |status|
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 '#ancestors' do
282
- it 'returns empty array if no parent' do
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.ancestors).to eq([])
296
+ expect { parent.disown(job) }.to raise_error
285
297
  end
286
298
 
287
- it 'returns entire job tree' do
288
- j1 = SideJob.queue('testq', 'TestWorker')
289
- j2 = SideJob.queue('testq', 'TestWorker', parent: j1, name: 'child')
290
- j3 = SideJob.queue('testq', 'TestWorker', parent: j2, name: 'child')
291
- j4 = SideJob.queue('testq', 'TestWorker', parent: j3, name: 'child')
292
- expect(j4.ancestors).to eq([j3, j2, j1])
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 '#terminated?' do
297
- before do
298
- @job = SideJob.queue('testq', 'TestWorker')
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 'returns false if job status is not terminated' do
302
- expect(@job.terminated?).to be false
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 'returns true if job status is terminated' do
306
- @job.status = 'terminated'
307
- expect(@job.terminated?).to be true
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 'returns false if child job is not terminated' do
311
- @job.status = 'terminated'
312
- SideJob.queue('testq', 'TestWorker', parent: @job, name: 'child')
313
- expect(@job.terminated?).to be false
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 'returns true if child job is terminated' do
317
- @job.status = 'terminated'
318
- child = SideJob.queue('testq', 'TestWorker', parent: @job, name: 'child')
319
- child.status = 'terminated'
320
- expect(@job.terminated?).to be true
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 cached #{type}put port" do
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"] = {'*' => {mode: :memory, default: 123}}
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: {mode: :memory, default: 'def'}})
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(@job).to receive(:config) { {"#{type}ports" => {'port1' => {}, 'port2' => {'mode' => 'memory'}}}}
415
- @job.send("#{type}ports=", {port2: {mode: :queue}, port3: {}})
416
- expect(@job.send("#{type}ports").size).to eq 3
417
- expect(@job.send("#{type}ports").all? {|port| port.options == {mode: :queue}}).to be true
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 mode while keeping data intact' do
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: {mode: :memory, default: 'def'}})
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).mode).to eq :memory
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 'def'
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
- it 'can specify port data' do
440
- @job.send("#{type}ports=", {myport: {data: [1,'abc']}})
441
- expect(@job.send("#{type}put", :myport).read).to eq 1
442
- expect(@job.send("#{type}put", :myport).read).to eq 'abc'
443
- end
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
- it 'groups logs' do
464
- now = Time.now
465
- allow(Time).to receive(:now) { now }
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
- SideJob.redis.hset 'jobs', @job.id, { queue: 'testq', class: 'TestWorker', field1: 'value1', field2: [1,2], field3: 123 }.to_json
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 'caches the state' do
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 123
522
+ expect(@job.get(:field3)).to eq 789
499
523
  end
524
+ end
500
525
 
501
- it 'raises error if job no longer exists and state is not cached' do
502
- @job.reload
503
- job2 = SideJob.find(@job.id)
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 'does not raise error if job no longer exists but state is cached' do
510
- @job.get(:foo)
511
- job2 = SideJob.find(@job.id)
512
- job2.status = 'terminated'
513
- job2.delete
514
- expect { @job.get(:key) }.not_to raise_error
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 '#reload' do
557
+ describe '#unset' do
519
558
  before do
520
559
  @job = SideJob.queue('testq', 'TestWorker')
521
560
  end
522
561
 
523
- it 'clears the job state cache' do
524
- expect(@job.get(:field1)).to be nil
525
- SideJob.redis.hset 'jobs', @job.id, {queue: 'testq', class: 'TestWorker', field1: 789}.to_json
526
- @job.reload
527
- expect(@job.get(:field1)).to eq 789
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 '#config' do
571
+ describe '#lock' do
532
572
  before do
533
573
  @job = SideJob.queue('testq', 'TestWorker')
534
574
  end
535
575
 
536
- it 'returns and caches worker configuration' do
537
- expect(SideJob::Worker).to receive(:config).with('testq', 'TestWorker').once.and_call_original
538
- @job.reload
539
- @job.config
540
- @job.config # test cached
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