sidejob 4.0.2 → 4.1.0

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