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.
@@ -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