sidejob 4.0.2 → 4.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/lib/sidejob/port.rb CHANGED
@@ -1,8 +1,10 @@
1
+ require 'delegate'
2
+
1
3
  module SideJob
2
4
  # Represents an input or output port from a Job
3
5
  class Port
4
6
  # Returned by {#read} and {#default} to indicate no data
5
- class None < Object; end
7
+ class None; end
6
8
 
7
9
  attr_reader :job, :type, :name
8
10
 
@@ -39,11 +41,10 @@ module SideJob
39
41
  size > 0 || default?
40
42
  end
41
43
 
42
- # Returns the port default value. To distinguish a null default value vs no default, use {#default?}.
43
- # @return [Object, None] The default value on the port or {SideJob::Port::None} if none
44
+ # Returns the port default value. See {.decode_data} for details about the return value.
45
+ # @return [Delegator, None] The default value on the port or {SideJob::Port::None} if none
44
46
  def default
45
- val = SideJob.redis.hget("#{@job.redis_key}:#{type}ports:default", @name)
46
- val ? parse_json(val) : None
47
+ self.class.decode_data SideJob.redis.hget("#{@job.redis_key}:#{type}ports:default", @name)
47
48
  end
48
49
 
49
50
  # Returns if the port has a default value.
@@ -58,36 +59,73 @@ module SideJob
58
59
  if val == None
59
60
  SideJob.redis.hdel "#{@job.redis_key}:#{type}ports:default", @name
60
61
  else
61
- SideJob.redis.hset "#{@job.redis_key}:#{type}ports:default", @name, val.to_json
62
+ SideJob.redis.hset "#{@job.redis_key}:#{type}ports:default", @name, self.class.encode_data(val)
63
+ end
64
+ end
65
+
66
+ # Returns the connected port channels.
67
+ # @return [Array<String>] List of port channels
68
+ def channels
69
+ JSON.parse(SideJob.redis.hget("#{@job.redis_key}:#{type}ports:channels", @name)) rescue []
70
+ end
71
+
72
+ # Set the channels connected to the port.
73
+ # @param channels [Array<String>] Port channels
74
+ def channels=(channels)
75
+ SideJob.redis.multi do |multi|
76
+ if channels && channels.length > 0
77
+ multi.hset "#{@job.redis_key}:#{type}ports:channels", @name, channels.to_json
78
+ else
79
+ multi.hdel "#{@job.redis_key}:#{type}ports:channels", @name
80
+ end
81
+
82
+ if type == :in
83
+ channels.each do |chan|
84
+ multi.sadd "channel:#{chan}", @job.id
85
+ end
86
+ end
62
87
  end
63
88
  end
64
89
 
65
- # Write data to the port. If port in an input port, runs the job.
90
+ # Write data to the port. If port in an input port, runs the job, otherwise run the parent job.
66
91
  # @param data [Object] JSON encodable data to write to the port
67
92
  def write(data)
68
- # For {SideJob::Worker#for_inputs}, if this is set, we instead set the port default on writes
69
- if Thread.current[:sidejob_port_write_default]
93
+ options = (Thread.current[:sidejob_port_group] || {})[:options] || {}
94
+ # For {SideJob::Worker#for_inputs}, if this option is set, we set the port default instead of pushing to the port
95
+ if options[:set_default]
70
96
  self.default = data
71
97
  else
72
- SideJob.redis.rpush redis_key, data.to_json
98
+ SideJob.redis.rpush redis_key, self.class.encode_data(data)
99
+ end
100
+
101
+ # run job if inport otherwise run parent
102
+ @job.run(parent: type != :in) unless options[:notify] == false
103
+
104
+ log(write: [ { port: self, data: [data] } ]) unless options[:log] == false
105
+
106
+ if type == :out
107
+ channels.each do |chan|
108
+ SideJob.publish chan, data
109
+ end
73
110
  end
74
- @job.run(parent: type != :in) # run job if inport otherwise run parent
75
- log(write: [ { port: self, data: [data] } ])
76
111
  end
77
112
 
78
- # Reads the oldest data from the port. Returns the default value if no data and there is a default.
79
- # @return [Object, None] First data from port or {SideJob::Port::None} if there is no data and no default.
113
+ # Reads the oldest data from the port. See {.decode_data} for details about the wrapped return value.
114
+ # Returns the {#default} if there is no port data and there is a default.
115
+ # Returns {SideJob::Port::None} if there is no port data and no default.
116
+ # @return [Delegator, None] First data from port or {SideJob::Port::None} if there is no data and no default
80
117
  def read
118
+ options = (Thread.current[:sidejob_port_group] || {})[:options] || {}
81
119
  data = SideJob.redis.lpop(redis_key)
82
120
  if data
83
- data = parse_json(data)
121
+ data = self.class.decode_data(data)
84
122
  elsif default?
85
123
  data = default
86
124
  else
87
125
  return None
88
126
  end
89
127
 
90
- log(read: [ { port: self, data: [data] } ])
128
+ log(read: [ { port: self, data: [data] } ]) unless options[:log] == false || data.sidejob_options['log'] == false
91
129
 
92
130
  data
93
131
  end
@@ -135,9 +173,18 @@ module SideJob
135
173
  end
136
174
  end
137
175
 
138
- data.map! {|x| parse_json x}
176
+ data.map! {|x| self.class.decode_data(x)}
139
177
  if data.length > 0
140
178
  log(read: [{ port: self, data: data }], write: ports.map { |port| {port: port, data: data} })
179
+
180
+ # Publish to destination channels
181
+ ports.each do |port|
182
+ if port.type == :out
183
+ port.channels.each do |chan|
184
+ data.each { |x| SideJob.publish chan, x }
185
+ end
186
+ end
187
+ end
141
188
  end
142
189
 
143
190
  # Run the port job or parent only if something was changed
@@ -171,53 +218,109 @@ module SideJob
171
218
  redis_key.hash
172
219
  end
173
220
 
174
- # Groups all port reads and writes within the block into a single logged event.
175
- def self.log_group(&block)
176
- outermost = ! Thread.current[:sidejob_port_group]
221
+ # Creates a group for port reads and write.
222
+ # All events inside the block are combined into a single logged event.
223
+ # Nested groups are not logged until the outermost group closes.
224
+ # Can pass additional options that are used for port read/writes inside the group.
225
+ # The default for all options is nil which means to inherit the current option value or its default.
226
+ # @param log [Boolean] If false, do not log the writing or reading of the data (default true)
227
+ # @param notify [Boolean] If false, do not notify (run) the port's job
228
+ # @param set_default [Boolean] If true, instead of writing to port, set default value
229
+ def self.group(log: nil, notify: nil, set_default: nil, &block)
230
+ previous_group = if Thread.current[:sidejob_port_group]
231
+ Thread.current[:sidejob_port_group].dup
232
+ else
233
+ nil
234
+ end
235
+
177
236
  Thread.current[:sidejob_port_group] ||= {read: {}, write: {}} # port -> [data]
237
+
238
+ options = if previous_group && previous_group[:options]
239
+ previous_group[:options].dup
240
+ else
241
+ {}
242
+ end
243
+ options[:log] = log unless log.nil?
244
+ options[:notify] = notify unless notify.nil?
245
+ options[:set_default] = set_default unless set_default.nil?
246
+ Thread.current[:sidejob_port_group][:options] = options
247
+
178
248
  yield
179
249
  ensure
180
- if outermost
181
- self._really_log Thread.current[:sidejob_port_group]
182
- Thread.current[:sidejob_port_group] = nil
250
+ if ! previous_group
251
+ group = Thread.current[:sidejob_port_group]
252
+ if group && (group[:read].length > 0 || group[:write].length > 0)
253
+ log_entry = {}
254
+ %i{read write}.each do |type|
255
+ log_entry[type] = group[type].map do |port, data|
256
+ x = {job: port.job.id, data: data}
257
+ x[:"#{port.type}port"] = port.name
258
+ x
259
+ end
260
+ end
261
+
262
+ SideJob.log log_entry
263
+ end
183
264
  end
265
+ Thread.current[:sidejob_port_group] = previous_group
184
266
  end
185
267
 
186
- private
187
-
188
- def self._really_log(entry)
189
- return unless entry && (entry[:read].length > 0 || entry[:write].length > 0)
268
+ # Encodes data as JSON with the current SideJob context.
269
+ # @param data [Object] JSON encodable data
270
+ # @return [String] The encoded JSON value
271
+ def self.encode_data(data)
272
+ encoded = { data: data }
273
+ encoded[:context] = Thread.current[:sidejob_context] if Thread.current[:sidejob_context]
274
+ if Thread.current[:sidejob_port_group] && Thread.current[:sidejob_port_group][:options]
275
+ encoded[:options] = Thread.current[:sidejob_port_group][:options]
276
+ end
277
+ encoded.to_json
278
+ end
190
279
 
191
- log_entry = {}
192
- %i{read write}.each do |type|
193
- log_entry[type] = entry[type].map do |port, data|
194
- x = {job: port.job.id, data: data}
195
- x[:"#{port.type}port"] = port.name
196
- x
280
+ # Decodes data encoded with {.encode_data}.
281
+ # The value is returned as a Delegator object that behaves mostly like the underlying value.
282
+ # Use {Delegator#__getobj__} to get directly at the underlying value.
283
+ # The returned delegator object has a sidejob_context method that returns the SideJob context
284
+ # and a sidejob_options method that returns the data options.
285
+ # @param data [String, nil] Data to decode
286
+ # @return [Delegator, None] The decoded value or {SideJob::Port::None} if data is nil
287
+ def self.decode_data(data)
288
+ if data
289
+ data = JSON.parse(data)
290
+ klass = Class.new(SimpleDelegator) do
291
+ # Allow comparing two SimpleDelegator objects
292
+ def ==(obj)
293
+ return self.__getobj__ == obj.__getobj__ if obj.is_a?(SimpleDelegator)
294
+ super
295
+ end
296
+ end
297
+ klass.send(:define_method, :sidejob_context) do
298
+ data['context'] || {}
197
299
  end
300
+ klass.send(:define_method, :sidejob_options) do
301
+ data['options'] || {}
302
+ end
303
+ klass.new(data['data'])
304
+ else
305
+ None
198
306
  end
199
-
200
- SideJob.log log_entry
201
307
  end
202
308
 
309
+ private
310
+
203
311
  def log(data)
204
- entry = Thread.current[:sidejob_port_group] ? Thread.current[:sidejob_port_group] : {read: {}, write: {}}
205
- %i{read write}.each do |type|
206
- (data[type] || []).each do |x|
207
- entry[type][x[:port]] ||= []
208
- entry[type][x[:port]].concat JSON.parse(x[:data].to_json) # serialize/deserialize to do a deep copy
312
+ if Thread.current[:sidejob_port_group]
313
+ %i{read write}.each do |type|
314
+ (data[type] || []).each do |x|
315
+ Thread.current[:sidejob_port_group][type][x[:port]] ||= []
316
+ Thread.current[:sidejob_port_group][type][x[:port]].concat JSON.parse(x[:data].to_json) # serialize/deserialize to do a deep copy
317
+ end
318
+ end
319
+ else
320
+ SideJob::Port.group do
321
+ log(data)
209
322
  end
210
323
  end
211
- if ! Thread.current[:sidejob_port_group]
212
- self.class._really_log(entry)
213
- end
214
- end
215
-
216
- # Wrapper around JSON.parse to also handle primitive types.
217
- # @param data [String] Data to parse
218
- # @return [Object, nil]
219
- def parse_json(data)
220
- JSON.parse("[#{data}]")[0]
221
324
  end
222
325
 
223
326
  # Check if the port exists, dynamically creating it if it does not exist and a * port exists for the job
@@ -5,6 +5,11 @@ module SideJob
5
5
  # For simplicity, a job is allowed to be queued multiple times in the Sidekiq queue
6
6
  # Only when it gets pulled out to be run, i.e. here, we decide if we want to actually run it
7
7
  class ServerMiddleware
8
+ class << self
9
+ # If true, we do not rescue or log errors and instead propagate errors (useful for testing)
10
+ attr_accessor :raise_errors
11
+ end
12
+
8
13
  # Called by sidekiq as a server middleware to handle running a worker
9
14
  # @param worker [SideJob::Worker]
10
15
  # @param msg [Hash] Sidekiq message format
@@ -29,7 +34,7 @@ module SideJob
29
34
  if token
30
35
  begin
31
36
  SideJob.redis.del "#{@worker.redis_key}:lock:worker"
32
- SideJob.log_context(job: @worker.id) do
37
+ SideJob.context(job: @worker.id) do
33
38
  case @worker.status
34
39
  when 'queued'
35
40
  run_worker { yield }
@@ -82,7 +87,11 @@ module SideJob
82
87
  @worker.terminate
83
88
  else
84
89
  # normal run
85
- @worker.set ran_at: SideJob.timestamp
90
+
91
+ # if ran_at is not set, then this is the first run of the job, so call the startup method if it exists
92
+ @worker.startup if @worker.respond_to?(:startup) && ! SideJob.redis.exists("#{@worker.redis_key}:ran_at")
93
+
94
+ SideJob.redis.set "#{@worker.redis_key}:ran_at", SideJob.timestamp
86
95
  @worker.status = 'running'
87
96
  yield
88
97
  @worker.status = 'completed' if @worker.status == 'running'
@@ -101,8 +110,11 @@ module SideJob
101
110
  end
102
111
 
103
112
  def add_exception(exception)
104
- # only store the backtrace until the first sidekiq line
105
- SideJob.log({ error: exception.message, backtrace: exception.backtrace.take_while {|l| l !~ /sidekiq/}.join("\n") })
113
+ if SideJob::ServerMiddleware.raise_errors
114
+ raise exception
115
+ else
116
+ SideJob.log exception
117
+ end
106
118
  end
107
119
  end
108
120
  end
@@ -31,22 +31,15 @@ module SideJob
31
31
  def run_inline(errors: true, queue: true, args: [])
32
32
  self.status = 'queued' if queue
33
33
 
34
- worker = get(:class).constantize.new
34
+ worker_info = JSON.parse(SideJob.redis.get("#{redis_key}:worker"))
35
+ worker = worker_info['class'].constantize.new
35
36
  worker.jid = id
36
- SideJob::ServerMiddleware.new.call(worker, {}, get(:queue)) do
37
+ SideJob::ServerMiddleware.raise_errors = errors
38
+ SideJob::ServerMiddleware.new.call(worker, {}, worker_info['queue']) do
37
39
  worker.perform(*args)
38
40
  end
39
-
40
- if errors && status == 'failed'
41
- SideJob.logs.each do |event|
42
- if event['error']
43
- exception = RuntimeError.exception(event['error'])
44
- exception.set_backtrace(event['backtrace'])
45
- raise exception
46
- end
47
- end
48
- raise "Job #{id} failed but cannot find error log"
49
- end
41
+ ensure
42
+ SideJob::ServerMiddleware.raise_errors = false
50
43
  end
51
44
  end
52
45
  end
@@ -1,4 +1,4 @@
1
1
  module SideJob
2
2
  # The current SideJob version
3
- VERSION = '4.0.2'
3
+ VERSION = '4.1.0'
4
4
  end
@@ -16,6 +16,9 @@ module SideJob
16
16
  multi.del "workers:#{queue}"
17
17
  multi.hmset "workers:#{queue}", @registry.map {|key, val| [key, val.to_json]}.flatten(1) if @registry.size > 0
18
18
  end
19
+ SideJob::Port.group(log: false) do
20
+ SideJob.publish "/sidejob/workers/#{queue}", @registry
21
+ end
19
22
  end
20
23
 
21
24
  # Returns the configuration registered for a worker.
@@ -83,7 +86,7 @@ module SideJob
83
86
  return unless inputs.length > 0
84
87
  ports = inputs.map {|name| input(name)}
85
88
  loop do
86
- SideJob::Port.log_group do
89
+ SideJob::Port.group do
87
90
  info = ports.map {|port| [ port.size > 0, port.default? ] }
88
91
 
89
92
  return unless info.any? {|x| x[0] || x[1]} # Nothing to do if there's no data to read
@@ -97,11 +100,8 @@ module SideJob
97
100
  last_default = get(:for_inputs) || []
98
101
  return unless defaults != last_default
99
102
  set({for_inputs: defaults})
100
- begin
101
- Thread.current[:sidejob_port_write_default] = true
103
+ SideJob::Port.group(set_default: true) do
102
104
  yield *defaults
103
- ensure
104
- Thread.current[:sidejob_port_write_default] = nil
105
105
  end
106
106
  return
107
107
  else
@@ -0,0 +1,36 @@
1
+ require 'spec_helper'
2
+
3
+ describe 'Channels'do
4
+ it 'Global pubsub via channels works correctly' do
5
+ job1 = SideJob.queue('testq', 'TestSum')
6
+ job2 = SideJob.queue('testq', 'TestSum')
7
+ job3 = SideJob.queue('testq', 'TestSum')
8
+ job4 = SideJob.queue('testq', 'TestSum')
9
+
10
+ job1.input(:ready).channels = [ '/test/ready' ]
11
+ job1.input(:in).channels = ['/test/in']
12
+ job1.output(:sum).channels = ['/test/chan1']
13
+
14
+ job2.input(:ready).channels = [ '/test/ready' ]
15
+ job2.input(:in).channels = ['/test/chan1']
16
+ job2.output(:sum).channels = ['/test/chan2']
17
+
18
+ job3.input(:ready).channels = [ '/test/ready' ]
19
+ job3.input(:in).channels = ['/test/chan1']
20
+ job3.output(:sum).channels = ['/test/chan2']
21
+
22
+ job4.input(:ready).channels = [ '/test/ready' ]
23
+ job4.input(:in).channels = ['/test/chan2']
24
+
25
+ [1,2,4].each {|x| SideJob.publish '/test/in', x}
26
+ SideJob.publish '/test/ready', true
27
+
28
+ job1.run_inline
29
+ job2.run_inline
30
+ job3.run_inline
31
+ job4.run_inline
32
+
33
+ expect(job4.status).to eq 'completed'
34
+ expect(job4.output(:sum).read).to eq(14)
35
+ end
36
+ end
@@ -65,11 +65,97 @@ describe SideJob::Job do
65
65
  end
66
66
 
67
67
  describe '#status=' do
68
- it 'sets status' do
68
+ before do
69
69
  @job = SideJob.queue('testq', 'TestWorker')
70
+ end
71
+
72
+ it 'sets status' do
70
73
  @job.status = 'newstatus'
71
74
  expect(@job.status).to eq 'newstatus'
72
75
  end
76
+
77
+ it 'publishes a message with status change' do
78
+ expect(SideJob).to receive(:publish).with("/sidejob/job/#{@job.id}", {status: 'newstatus'})
79
+ @job.status = 'newstatus'
80
+ end
81
+
82
+ it 'does not publish a message if status does not change' do
83
+ @job.status = 'newstatus'
84
+ expect(SideJob).not_to receive(:publish)
85
+ @job.status = 'newstatus'
86
+ end
87
+
88
+ it 'does not publish a message if worker has status_publish: false' do
89
+
90
+ end
91
+ end
92
+
93
+ describe '#aliases' do
94
+ before do
95
+ @job = SideJob.queue('testq', 'TestWorker')
96
+ end
97
+
98
+ it 'returns empty array if no aliases' do
99
+ expect(@job.aliases).to eq []
100
+ end
101
+
102
+ it 'returns all aliases' do
103
+ @job.add_alias('abc')
104
+ @job.add_alias('xyz')
105
+ expect(@job.aliases).to match_array ['abc', 'xyz']
106
+ end
107
+ end
108
+
109
+ describe '#add_alias' do
110
+ before do
111
+ @job = SideJob.queue('testq', 'TestWorker')
112
+ @job.add_alias('abc')
113
+ end
114
+
115
+ it 'adds an alias' do
116
+ expect(SideJob.redis.smembers("#{@job.redis_key}:aliases")).to eq ['abc']
117
+ expect(SideJob.redis.hgetall('jobs:aliases')).to eq({'abc' => @job.id.to_s})
118
+ @job.add_alias('xyz')
119
+ expect(SideJob.redis.smembers("#{@job.redis_key}:aliases")).to match_array ['abc', 'xyz']
120
+ expect(SideJob.redis.hgetall('jobs:aliases')).to eq({'abc' => @job.id.to_s, 'xyz' => @job.id.to_s})
121
+ expect(@job.aliases).to match_array ['abc', 'xyz']
122
+ end
123
+
124
+ it 'does nothing if name is already alias' do
125
+ @job.add_alias('abc')
126
+ expect(SideJob.redis.smembers("#{@job.redis_key}:aliases")).to eq ['abc']
127
+ expect(SideJob.redis.hgetall('jobs:aliases')).to eq({'abc' => @job.id.to_s})
128
+ end
129
+
130
+ it 'raises error if name is an alias for another job' do
131
+ @job2 = SideJob.queue('testq', 'TestWorker')
132
+ expect { @job2.alias_as('abc') }.to raise_error
133
+ end
134
+
135
+ it 'raises error if name is invalid' do
136
+ expect { @job.add_alias('1234') }.to raise_error
137
+ expect { @job.add_alias('!') }.to raise_error
138
+ expect { @job.add_alias('') }.to raise_error
139
+ expect { @job.add_alias(nil) }.to raise_error
140
+ end
141
+ end
142
+
143
+ describe '#remove_alias' do
144
+ before do
145
+ @job = SideJob.queue('testq', 'TestWorker')
146
+ @job.add_alias('abc')
147
+ end
148
+
149
+ it 'removes an existing alias' do
150
+ @job.add_alias('xyz')
151
+ expect(@job.aliases).to match_array ['abc', 'xyz']
152
+ @job.remove_alias('xyz')
153
+ expect(@job.aliases).to match_array ['abc']
154
+ end
155
+
156
+ it 'throws an error if alias does not exist' do
157
+ expect { @job.remove_alias('xyz') }.to raise_error
158
+ end
73
159
  end
74
160
 
75
161
  describe '#run' do
@@ -133,7 +219,7 @@ describe SideJob::Job do
133
219
  end
134
220
 
135
221
  it 'throws error and immediately sets status to terminated if job class is unregistered' do
136
- SideJob.redis.del "workers:#{@job.get(:queue)}"
222
+ SideJob.redis.del "workers:#{@job.info[:queue]}"
137
223
  expect { @job.run }.to raise_error
138
224
  expect(@job.status).to eq 'terminated'
139
225
  end
@@ -258,7 +344,7 @@ describe SideJob::Job do
258
344
 
259
345
  it 'queues with by string set to self' do
260
346
  child = @job.queue('testq', 'TestWorker', name: 'child')
261
- expect(child.get(:created_by)).to eq "job:#{@job.id}"
347
+ expect(child.info[:created_by]).to eq "job:#{@job.id}"
262
348
  end
263
349
  end
264
350
 
@@ -371,8 +457,15 @@ describe SideJob::Job do
371
457
  expect(@job.exists?).to be false
372
458
  end
373
459
 
460
+ it 'publishes a message when deleted' do
461
+ @job.status = 'terminated'
462
+ expect(@job).to receive(:publish).with({deleted: true})
463
+ expect(@job.delete).to be true
464
+ end
465
+
374
466
  it 'recursively deletes jobs' do
375
467
  child = SideJob.queue('testq', 'TestWorker', parent: @job, name: 'child')
468
+ child.add_alias('myjob')
376
469
  expect(@job.status).to eq('queued')
377
470
  expect(child.status).to eq('queued')
378
471
  expect(SideJob.redis {|redis| redis.keys('job:*').length}).to be > 0
@@ -382,6 +475,7 @@ describe SideJob::Job do
382
475
  expect(SideJob.redis {|redis| redis.keys('job:*').length}).to be(0)
383
476
  expect(@job.exists?).to be false
384
477
  expect(child.exists?).to be false
478
+ expect(SideJob.find('myjob')).to be nil
385
479
  end
386
480
 
387
481
  it 'deletes data on input and output ports' do
@@ -401,6 +495,15 @@ describe SideJob::Job do
401
495
  child.delete
402
496
  expect(@job.children.size).to eq 0
403
497
  end
498
+
499
+ it 'removes any aliases' do
500
+ @job.add_alias 'job'
501
+ @job.status = 'terminated'
502
+ expect(SideJob.find('job')).to eq @job
503
+ @job.delete
504
+ expect(SideJob.find('job')).to be nil
505
+ expect(SideJob.redis.hget('jobs:aliases', 'job')).to be nil
506
+ end
404
507
  end
405
508
 
406
509
  # Tests are identical for input and output port methods
@@ -471,6 +574,15 @@ describe SideJob::Job do
471
574
  expect(@job.send("#{type}put", :myport).read).to eq 'new'
472
575
  end
473
576
 
577
+ it 'sets port channels' do
578
+ @channels = ['abc', 'xyz']
579
+ @job.send("#{type}ports=", {myport: {channels: @channels}})
580
+ expect(@job.send("#{type}put", :myport).channels).to match_array(@channels)
581
+ @channels.each do |chan|
582
+ expect(SideJob.redis.smembers("channel:#{chan}")).to eq [@job.id.to_s]
583
+ end
584
+ end
585
+
474
586
  it 'deletes no longer used ports' do
475
587
  @job.send("#{type}ports=", {myport: {}})
476
588
  @job.send("#{type}put", :myport).write 123
@@ -481,16 +593,28 @@ describe SideJob::Job do
481
593
  end
482
594
  end
483
595
 
484
- describe '#state' do
596
+ describe '#info' do
485
597
  before do
486
598
  now = Time.now
487
599
  allow(Time).to receive(:now) { now }
600
+ @job = SideJob.queue('testq', 'TestWorker', args: [123], by: 'me')
601
+ end
602
+
603
+ it 'returns all basic job info' do
604
+ expect(@job.info).to eq({queue: 'testq', class: 'TestWorker', args: [123],
605
+ created_by: 'me', created_at: SideJob.timestamp, ran_at: nil})
606
+ end
607
+ end
608
+
609
+ describe '#state' do
610
+ before do
488
611
  @job = SideJob.queue('testq', 'TestWorker')
489
612
  end
490
613
 
491
- it 'returns job state with common and internal keys' do
614
+ it 'returns all job state' do
492
615
  @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})
616
+ @job.set({xyz: 456})
617
+ expect(@job.state).to eq({'abc' => 123, 'xyz' => 456})
494
618
  end
495
619
  end
496
620
 
@@ -518,7 +642,7 @@ describe SideJob::Job do
518
642
 
519
643
  it 'always returns the latest value' do
520
644
  expect(@job.get(:field3)).to eq 123
521
- SideJob.redis.hmset @job.redis_key, :field3, '789'
645
+ SideJob.find(@job.id).set(field3: 789)
522
646
  expect(@job.get(:field3)).to eq 789
523
647
  end
524
648
  end
@@ -543,7 +667,7 @@ describe SideJob::Job do
543
667
  3.times do |i|
544
668
  @job.set key: [i]
545
669
  expect(@job.get(:key)).to eq [i]
546
- expect(JSON.parse(SideJob.redis.hget(@job.redis_key, 'key'))).to eq [i]
670
+ expect(JSON.parse(SideJob.redis.hget("#{@job.redis_key}:state", 'key'))).to eq [i]
547
671
  end
548
672
  end
549
673
 
@@ -622,4 +746,13 @@ describe SideJob::Job do
622
746
  expect(SideJob.redis.exists("#{@job.redis_key}:lock")).to be true
623
747
  end
624
748
  end
749
+
750
+ describe '#publish' do
751
+ it 'calls SideJob.publish' do
752
+ @job = SideJob.queue('testq', 'TestWorker')
753
+ message = {abc: 123}
754
+ expect(SideJob).to receive(:publish).with("/sidejob/job/#{@job.id}", message)
755
+ @job.publish(message)
756
+ end
757
+ end
625
758
  end