sidejob 4.0.2 → 4.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +38 -15
- data/lib/sidejob.rb +82 -26
- data/lib/sidejob/job.rb +121 -33
- data/lib/sidejob/port.rb +153 -50
- data/lib/sidejob/server_middleware.rb +16 -4
- data/lib/sidejob/testing.rb +6 -13
- data/lib/sidejob/version.rb +1 -1
- data/lib/sidejob/worker.rb +5 -5
- data/spec/integration/channels_spec.rb +36 -0
- data/spec/sidejob/job_spec.rb +141 -8
- data/spec/sidejob/port_spec.rb +208 -68
- data/spec/sidejob/server_middleware_spec.rb +30 -15
- data/spec/sidejob/testing_spec.rb +0 -18
- data/spec/sidejob/worker_spec.rb +11 -6
- data/spec/sidejob_spec.rb +85 -33
- data/web/Gemfile.lock +2 -2
- data/web/app.rb +22 -19
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e731ae1460c300e29d0698535fd1b7108efd3568
|
4
|
+
data.tar.gz: be777ac02165ead0b44de9100cefb8dfbb3c16ac
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 82265dc59bf18956bff558a9e9c996d74ae4a7a2f70f396d6a6188a73c884743756c57fac40bbba958de9bc9fe95e5aca435a3ae4489d23b56700c8f3b330448
|
7
|
+
data.tar.gz: baac41c609404f39cc9083f13dd489856faaa34f76db2fc5774e1190cf0c9ddbbdab036d893a77a22749a92f5670a62cc04e033e9367eb3ab3f6b1ecf3368cac
|
data/README.md
CHANGED
@@ -18,6 +18,7 @@ Jobs
|
|
18
18
|
* This ID is used as Sidekiq's jid
|
19
19
|
* Note: a job can be queued multiple times on Sidekiq's queues
|
20
20
|
* Therefore, Sidekiq's jids are not unique
|
21
|
+
* Jobs can also have any number of globally unique string names as aliases
|
21
22
|
* Jobs have a queue and class name
|
22
23
|
* Jobs have any number of input and output ports
|
23
24
|
* A job can have any number of named child jobs
|
@@ -48,6 +49,29 @@ Ports
|
|
48
49
|
a port named `*` exists in which case new ports are dynamically created and inherit its options.
|
49
50
|
* Currently, the only port option is a default value which is returned when a read is done on the port when its empty.
|
50
51
|
|
52
|
+
Channels
|
53
|
+
--------
|
54
|
+
|
55
|
+
Channels provide a global reliable pubsub system. Every port can be associated with some number of channels.
|
56
|
+
Writes to output ports will publish the data to the associated channels. Any published messages to a channel
|
57
|
+
are written to all input ports that have subscribed to that channel.
|
58
|
+
|
59
|
+
The pubsub system is reliable in that the subscribed jobs do not need to be running to receive messages.
|
60
|
+
Other clients can also subscribe to channels via standard non-reliable Redis pubsub.
|
61
|
+
|
62
|
+
Channel names use slashes to indicate hierarchy. Published messages to a channel are also published to channels
|
63
|
+
up the hierarchy. For example, a message sent to the channel `/namespace/event` will be sent to the channels
|
64
|
+
`/namespace/event`, `/namespace` and `/`.
|
65
|
+
|
66
|
+
SideJob uses channels starting with /sidejob. The channels used by sidejob:
|
67
|
+
|
68
|
+
* `/sidejob/log` : Log message. In the context of a running job, the job id will be included in the log entry.
|
69
|
+
* { timestamp: (date), read: [{ job: (id), (in|out)port: (port), data: [...] }, ...], write: [{ job: (id), (in|out)port: (port), data: [...] }, ...] }
|
70
|
+
* { timestamp: (date), error: (message), backtrace: (exception backtrace) }
|
71
|
+
* { timestamp: (date), message: (message) }
|
72
|
+
* `/sidejob/workers/[queue]` : Worker registry updated for the queue
|
73
|
+
* `/sidejob/job/[id]` : Job messages
|
74
|
+
|
51
75
|
Workers
|
52
76
|
-------
|
53
77
|
|
@@ -55,6 +79,7 @@ Workers
|
|
55
79
|
* Workers are required to register themselves
|
56
80
|
* A Sidekiq process should only handle a single queue so all registered workers in the process are for the same queue
|
57
81
|
* It should have a perform method that is called on each run
|
82
|
+
* It may have a startup method that is called once before the first run of the job
|
58
83
|
* It may have a shutdown method that is called before the job is terminated
|
59
84
|
* Workers should be idempotent as they may be run more than once for the same state
|
60
85
|
* SideJob ensures only one worker thread runs for a given job at a time
|
@@ -94,28 +119,26 @@ Additional keys used by SideJob:
|
|
94
119
|
|
95
120
|
* workers:(queue) - Hash mapping class name to worker configuration. A worker should define
|
96
121
|
the inports and outports hashes that map port names to port options.
|
97
|
-
* jobs
|
98
|
-
* jobs:
|
99
|
-
|
100
|
-
|
101
|
-
*
|
102
|
-
* job:(id) -
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
* args - array of arguments passed to worker's perform method
|
107
|
-
* parent - parent job ID
|
108
|
-
* created_at - timestamp that the job was first queued
|
109
|
-
* created_by - string indicating the entity that created the job. SideJob uses job:(id) for jobs created by another job.
|
110
|
-
* ran_at - timestamp of the start of the last run
|
111
|
-
* Any additional keys used by the worker to track internal job state
|
122
|
+
* jobs - Set with all job ids.
|
123
|
+
* jobs:last_id - Stores the last job ID (we use incrementing integers from 1).
|
124
|
+
* jobs:aliases - Hash mapping a name to job id.
|
125
|
+
* job:(id):worker - JSON encoded hash with queue, class, and args for calling the worker.
|
126
|
+
* job:(id):status - Job status
|
127
|
+
* job:(id):created_at - Timestamp that the job was first queued
|
128
|
+
* job:(id):created_by - The entity that created the job. SideJob uses job:(id) for jobs created by another job.
|
129
|
+
* job:(id):ran_at - Timestamp of the start of the last run
|
130
|
+
* job:(id):aliases - Set with job aliases
|
112
131
|
* job:(id):in:(inport) and job:(id):out:(outport) - List with unread port data. New data is pushed on the right.
|
113
132
|
* job:(id):inports and job:(id):outports - Set containing all existing port names.
|
114
133
|
* job:(id):inports:default and job:(id):outports:default - Hash mapping port name to JSON encoded default value for port.
|
134
|
+
* job:(id):inports:channels and job:(id):outports:channels - Hash mapping port name to JSON encoded connected channels.
|
135
|
+
* job:(id):parent - Parent job ID
|
115
136
|
* job:(id):children - Hash mapping child job name to child job ID
|
137
|
+
* job:(id):state - Hash containing job specific internal state. Each value is JSON encoded.
|
116
138
|
* job:(id):rate:(timestamp) - Rate limiter used to prevent run away executing of a job.
|
117
139
|
Keys are automatically expired.
|
118
140
|
* job:(id):lock - Used to control concurrent writes to a job.
|
119
141
|
Auto expired to prevent stale locks.
|
120
142
|
* job:(id):lock:worker - Used to indicate a worker is attempting to acquire the job lock.
|
121
143
|
Auto expired to prevent stale locks.
|
144
|
+
* channel:(channel) - Set with job ids that may have ports subscribed to the channel.
|
data/lib/sidejob.rb
CHANGED
@@ -7,6 +7,7 @@ require 'sidejob/worker'
|
|
7
7
|
require 'sidejob/server_middleware'
|
8
8
|
require 'time' # for iso8601 method
|
9
9
|
require 'securerandom'
|
10
|
+
require 'pathname'
|
10
11
|
|
11
12
|
module SideJob
|
12
13
|
# Configuration parameters
|
@@ -49,11 +50,17 @@ module SideJob
|
|
49
50
|
|
50
51
|
# To prevent race conditions, we generate the id and set all data in redis before queuing the job to sidekiq
|
51
52
|
# Otherwise, sidekiq may start the job too quickly
|
52
|
-
id = SideJob.redis.incr('jobs:last_id')
|
53
|
+
id = SideJob.redis.incr('jobs:last_id')
|
53
54
|
SideJob.redis.sadd 'jobs', id
|
54
55
|
job = SideJob::Job.new(id)
|
55
56
|
|
56
|
-
|
57
|
+
redis_key = job.redis_key
|
58
|
+
SideJob.redis.multi do |multi|
|
59
|
+
multi.set "#{redis_key}:worker", {queue: queue, class: klass, args: args}.to_json
|
60
|
+
multi.set "#{redis_key}:status", 'completed'
|
61
|
+
multi.set "#{redis_key}:created_at", SideJob.timestamp
|
62
|
+
multi.set "#{redis_key}:created_by", by
|
63
|
+
end
|
57
64
|
|
58
65
|
if parent
|
59
66
|
raise 'Missing name option for job with a parent' unless name
|
@@ -67,12 +74,11 @@ module SideJob
|
|
67
74
|
job.run(at: at)
|
68
75
|
end
|
69
76
|
|
70
|
-
# Finds a job by id
|
71
|
-
# @param
|
77
|
+
# Finds a job by name or id.
|
78
|
+
# @param name_or_id [String, Integer] Job name or id
|
72
79
|
# @return [SideJob::Job, nil] Job object or nil if it doesn't exist
|
73
|
-
def self.find(
|
74
|
-
|
75
|
-
job = SideJob::Job.new(job_id) rescue nil
|
80
|
+
def self.find(name_or_id)
|
81
|
+
SideJob::Job.new(name_or_id) rescue nil
|
76
82
|
end
|
77
83
|
|
78
84
|
# Returns the current timestamp as a iso8601 string
|
@@ -81,31 +87,81 @@ module SideJob
|
|
81
87
|
Time.now.utc.iso8601(9)
|
82
88
|
end
|
83
89
|
|
84
|
-
#
|
85
|
-
# @param entry [Hash] Log entry
|
90
|
+
# Publishes a log message using the current SideJob context.
|
91
|
+
# @param entry [Hash|Exception|String] Log entry
|
86
92
|
def self.log(entry)
|
87
|
-
context = (Thread.current[:
|
88
|
-
|
89
|
-
|
93
|
+
context = (Thread.current[:sidejob_context] || {}).merge(timestamp: SideJob.timestamp)
|
94
|
+
|
95
|
+
if entry.is_a?(Exception)
|
96
|
+
exception = entry
|
97
|
+
entry = { error: exception.message }
|
98
|
+
if exception.backtrace
|
99
|
+
# only store the backtrace until the first sidekiq line
|
100
|
+
entry[:backtrace] = exception.backtrace.take_while {|l| l !~ /sidekiq/}.join("\n")
|
101
|
+
end
|
102
|
+
elsif entry.is_a?(String)
|
103
|
+
entry = { message: entry }
|
104
|
+
end
|
90
105
|
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
SideJob.redis.multi do |multi|
|
96
|
-
multi.lrange 'jobs:logs', 0, -1
|
97
|
-
multi.del 'jobs:logs' if clear
|
98
|
-
end[0].map {|log| JSON.parse(log)}
|
106
|
+
# Disable logging to prevent infinite publish loop for input ports subscribed to /sidejob/log which could generate log entries
|
107
|
+
SideJob::Port.group(log: false) do
|
108
|
+
SideJob.publish '/sidejob/log', context.merge(entry)
|
109
|
+
end
|
99
110
|
end
|
100
111
|
|
101
|
-
# Adds the
|
102
|
-
# @param
|
103
|
-
def self.
|
104
|
-
previous = Thread.current[:
|
105
|
-
Thread.current[:
|
112
|
+
# Adds to the current SideJob context within the block.
|
113
|
+
# @param data [Hash] Data to be merged into the current context
|
114
|
+
def self.context(data, &block)
|
115
|
+
previous = Thread.current[:sidejob_context]
|
116
|
+
Thread.current[:sidejob_context] = (previous || {}).merge(data.symbolize_keys)
|
106
117
|
yield
|
107
118
|
ensure
|
108
|
-
Thread.current[:
|
119
|
+
Thread.current[:sidejob_context] = previous
|
120
|
+
end
|
121
|
+
|
122
|
+
# Publishes a message up the channel hierarchy to jobs by writing to ports subscribed to the channel.
|
123
|
+
# Also publishes to the destination channel only via normal redis pubsub.
|
124
|
+
# @param channel [String] Channel is path-like, separated by / to indicate hierarchy
|
125
|
+
# @param message [Object] JSON encodable message
|
126
|
+
def self.publish(channel, message)
|
127
|
+
# We don't publish at every level up hierarchy via redis pubsub since a client can use redis psubscribe
|
128
|
+
SideJob.redis.publish channel, message.to_json
|
129
|
+
|
130
|
+
job_subs = {}
|
131
|
+
|
132
|
+
# Set the context to the original channel so that a job that subscribes to a higher channel can determine
|
133
|
+
# the original channel that the message was sent to.
|
134
|
+
SideJob.context({channel: channel}) do
|
135
|
+
# walk up the channel hierarchy
|
136
|
+
Pathname.new(channel).ascend do |channel|
|
137
|
+
channel = channel.to_s
|
138
|
+
jobs = SideJob.redis.smembers "channel:#{channel}"
|
139
|
+
jobs.each do |id|
|
140
|
+
job = SideJob.find(id)
|
141
|
+
if ! job_subs.has_key?(id)
|
142
|
+
job_subs[id] = {}
|
143
|
+
if job
|
144
|
+
SideJob.redis.hgetall("#{job.redis_key}:inports:channels").each_pair do |port, channels|
|
145
|
+
channels = JSON.parse(channels)
|
146
|
+
channels.each do |ch|
|
147
|
+
job_subs[id][ch] ||= []
|
148
|
+
job_subs[id][ch] << port
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
if job && job_subs[id] && job_subs[id][channel]
|
155
|
+
job_subs[id][channel].each do |port|
|
156
|
+
job.input(port).write message
|
157
|
+
end
|
158
|
+
else
|
159
|
+
# Job is gone or no longer subscribed to this channel
|
160
|
+
SideJob.redis.srem "channel:#{channel}", id
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|
109
165
|
end
|
110
166
|
end
|
111
167
|
|
data/lib/sidejob/job.rb
CHANGED
@@ -33,13 +33,55 @@ module SideJob
|
|
33
33
|
# Retrieve the job's status.
|
34
34
|
# @return [String] Job status
|
35
35
|
def status
|
36
|
-
|
36
|
+
check_exists
|
37
|
+
SideJob.redis.get "#{redis_key}:status"
|
37
38
|
end
|
38
39
|
|
39
40
|
# Set the job status.
|
40
41
|
# @param status [String] The new job status
|
41
42
|
def status=(status)
|
42
|
-
|
43
|
+
check_exists
|
44
|
+
oldstatus = SideJob.redis.getset("#{redis_key}:status", status)
|
45
|
+
if oldstatus != status && worker_config['status_publish'] != false
|
46
|
+
SideJob::Port.group(log: false) do
|
47
|
+
publish({status: status})
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# Returns all aliases for the job.
|
53
|
+
# @return [Array<String>] Job aliases
|
54
|
+
def aliases
|
55
|
+
SideJob.redis.smembers "#{redis_key}:aliases"
|
56
|
+
end
|
57
|
+
|
58
|
+
# Add an alias for the job.
|
59
|
+
# @param name [String] Alias for the job. Must begin with an alphabetic character.
|
60
|
+
# @raise [RuntimeError] Error if name is invalid or the name already refers to another job
|
61
|
+
def add_alias(name)
|
62
|
+
check_exists
|
63
|
+
raise "#{name} is not a valid alias" unless name =~ /^[[:alpha:]]/
|
64
|
+
current = SideJob.redis.hget('jobs:aliases', name)
|
65
|
+
if current
|
66
|
+
raise "#{name} is already used by job #{current}" if current.to_i != id
|
67
|
+
else
|
68
|
+
SideJob.redis.multi do |multi|
|
69
|
+
multi.hset 'jobs:aliases', name, id
|
70
|
+
multi.sadd "#{redis_key}:aliases", name
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
# Remove an alias for the job.
|
76
|
+
# @param name [String] Alias to remove for the job
|
77
|
+
# @raise [RuntimeError] Error if name is not an alias for this job
|
78
|
+
def remove_alias(name)
|
79
|
+
check_exists
|
80
|
+
raise "#{name} is not an alias for job #{id}" unless SideJob.redis.sismember("#{redis_key}:aliases", name)
|
81
|
+
SideJob.redis.multi do |multi|
|
82
|
+
multi.hdel 'jobs:aliases', name
|
83
|
+
multi.srem "#{redis_key}:aliases", name
|
84
|
+
end
|
43
85
|
end
|
44
86
|
|
45
87
|
# Run the job.
|
@@ -53,8 +95,6 @@ module SideJob
|
|
53
95
|
# @param wait [Float] Run in the specified number of seconds
|
54
96
|
# @return [SideJob::Job, nil] The job that was run or nil if no job was run
|
55
97
|
def run(parent: false, force: false, at: nil, wait: nil)
|
56
|
-
check_exists
|
57
|
-
|
58
98
|
if parent
|
59
99
|
pj = self.parent
|
60
100
|
return pj ? pj.run(force: force, at: at, wait: wait) : nil
|
@@ -110,6 +150,7 @@ module SideJob
|
|
110
150
|
# Queues a child job, setting parent and by to self.
|
111
151
|
# @see SideJob.queue
|
112
152
|
def queue(queue, klass, **options)
|
153
|
+
check_exists
|
113
154
|
SideJob.queue(queue, klass, options.merge({parent: self, by: "job:#{id}"}))
|
114
155
|
end
|
115
156
|
|
@@ -129,9 +170,7 @@ module SideJob
|
|
129
170
|
# Returns the parent job.
|
130
171
|
# @return [SideJob::Job, nil] Parent job or nil if none
|
131
172
|
def parent
|
132
|
-
|
133
|
-
parent = SideJob.find(parent) if parent
|
134
|
-
parent
|
173
|
+
SideJob.find(SideJob.redis.get("#{redis_key}:parent"))
|
135
174
|
end
|
136
175
|
|
137
176
|
# Disown a child job so that it no longer has a parent.
|
@@ -148,7 +187,7 @@ module SideJob
|
|
148
187
|
end
|
149
188
|
|
150
189
|
SideJob.redis.multi do |multi|
|
151
|
-
multi.
|
190
|
+
multi.del "#{job.redis_key}:parent"
|
152
191
|
multi.hdel "#{redis_key}:children", name
|
153
192
|
end
|
154
193
|
end
|
@@ -157,12 +196,13 @@ module SideJob
|
|
157
196
|
# @param orphan [SideJob::Job] Job that has no parent
|
158
197
|
# @param name [String] Name of child job (must be unique among children)
|
159
198
|
def adopt(orphan, name)
|
199
|
+
check_exists
|
160
200
|
raise "Job #{id} cannot adopt itself as a child" if orphan == self
|
161
201
|
raise "Job #{id} cannot adopt job #{orphan.id} as it already has a parent" unless orphan.parent.nil?
|
162
202
|
raise "Job #{id} cannot adopt job #{orphan.id} as child name #{name} is not unique" if name.nil? || ! child(name).nil?
|
163
203
|
|
164
204
|
SideJob.redis.multi do |multi|
|
165
|
-
multi.
|
205
|
+
multi.set "#{orphan.redis_key}:parent", id.to_json
|
166
206
|
multi.hset "#{redis_key}:children", name, orphan.id
|
167
207
|
end
|
168
208
|
end
|
@@ -176,14 +216,16 @@ module SideJob
|
|
176
216
|
parent.disown(self) if parent
|
177
217
|
|
178
218
|
children = self.children
|
219
|
+
aliases = self.aliases
|
179
220
|
|
180
221
|
# delete all SideJob keys and disown all children
|
181
222
|
ports = inports.map(&:redis_key) + outports.map(&:redis_key)
|
182
223
|
SideJob.redis.multi do |multi|
|
183
224
|
multi.srem 'jobs', id
|
184
225
|
multi.del redis_key
|
185
|
-
multi.del ports + %w{children inports outports inports:default outports:default}.map {|x| "#{redis_key}:#{x}" }
|
226
|
+
multi.del ports + %w{worker status state aliases parent children inports outports inports:default outports:default inports:channels outports:channels created_at created_by ran_at}.map {|x| "#{redis_key}:#{x}" }
|
186
227
|
children.each_value { |child| multi.hdel child.redis_key, 'parent' }
|
228
|
+
aliases.each { |name| multi.hdel('jobs:aliases', name) }
|
187
229
|
end
|
188
230
|
|
189
231
|
# recursively delete all children
|
@@ -191,6 +233,7 @@ module SideJob
|
|
191
233
|
child.delete
|
192
234
|
end
|
193
235
|
|
236
|
+
publish({deleted: true})
|
194
237
|
return true
|
195
238
|
end
|
196
239
|
|
@@ -236,20 +279,39 @@ module SideJob
|
|
236
279
|
set_ports :out, ports
|
237
280
|
end
|
238
281
|
|
239
|
-
# Returns
|
240
|
-
# @return [Hash
|
282
|
+
# Returns
|
283
|
+
# @return [Hash]
|
284
|
+
def info
|
285
|
+
check_exists
|
286
|
+
data = SideJob.redis.multi do |multi|
|
287
|
+
multi.get "#{redis_key}:worker"
|
288
|
+
multi.get "#{redis_key}:created_by"
|
289
|
+
multi.get "#{redis_key}:created_at"
|
290
|
+
multi.get "#{redis_key}:ran_at"
|
291
|
+
end
|
292
|
+
|
293
|
+
worker = JSON.parse(data[0])
|
294
|
+
{
|
295
|
+
queue: worker['queue'], class: worker['class'], args: worker['args'],
|
296
|
+
created_by: data[1], created_at: data[2], ran_at: data[3],
|
297
|
+
}
|
298
|
+
end
|
299
|
+
|
300
|
+
# Returns the entirety of the job's internal state.
|
301
|
+
# @return [Hash{String => Object}] Job internal state
|
241
302
|
def state
|
242
|
-
|
243
|
-
|
303
|
+
check_exists
|
304
|
+
state = SideJob.redis.hgetall("#{redis_key}:state")
|
244
305
|
state.update(state) {|k,v| JSON.parse("[#{v}]")[0]}
|
245
306
|
state
|
246
307
|
end
|
247
308
|
|
248
|
-
# Returns some data from the job's state.
|
309
|
+
# Returns some data from the job's internal state.
|
249
310
|
# @param key [Symbol,String] Retrieve value for the given key
|
250
311
|
# @return [Object,nil] Value from the job state or nil if key does not exist
|
251
312
|
def get(key)
|
252
|
-
|
313
|
+
check_exists
|
314
|
+
val = SideJob.redis.hget("#{redis_key}:state", key)
|
253
315
|
val ? JSON.parse("[#{val}]")[0] : nil
|
254
316
|
end
|
255
317
|
|
@@ -259,7 +321,7 @@ module SideJob
|
|
259
321
|
def set(data)
|
260
322
|
check_exists
|
261
323
|
return unless data.size > 0
|
262
|
-
SideJob.redis.hmset redis_key, *(data.map {|k,v| [k, v.to_json]}.flatten)
|
324
|
+
SideJob.redis.hmset "#{redis_key}:state", *(data.map {|k,v| [k, v.to_json]}.flatten)
|
263
325
|
end
|
264
326
|
|
265
327
|
# Unsets some fields in the job's internal state.
|
@@ -267,7 +329,7 @@ module SideJob
|
|
267
329
|
# @raise [RuntimeError] Error raised if job no longer exists
|
268
330
|
def unset(*fields)
|
269
331
|
return unless fields.length > 0
|
270
|
-
SideJob.redis.hdel redis_key, fields
|
332
|
+
SideJob.redis.hdel "#{redis_key}:state", fields
|
271
333
|
end
|
272
334
|
|
273
335
|
# Acquire a lock on the job with a given expiration time.
|
@@ -276,6 +338,7 @@ module SideJob
|
|
276
338
|
# @param retry_delay [Float] Maximum seconds to wait (actual will be randomized) before retry getting lock
|
277
339
|
# @return [String, nil] Lock token that should be passed to {#unlock} or nil if lock was not acquired
|
278
340
|
def lock(ttl, retries: 3, retry_delay: 0.2)
|
341
|
+
check_exists
|
279
342
|
retries.times do
|
280
343
|
token = SecureRandom.uuid
|
281
344
|
if SideJob.redis.set("#{redis_key}:lock", token, {nx: true, ex: ttl})
|
@@ -291,6 +354,7 @@ module SideJob
|
|
291
354
|
# @param ttl [Fixnum] Refresh lock expiration for the given time in seconds
|
292
355
|
# @return [Boolean] Whether the timeout was set
|
293
356
|
def refresh_lock(ttl)
|
357
|
+
check_exists
|
294
358
|
SideJob.redis.expire "#{redis_key}:lock", ttl
|
295
359
|
end
|
296
360
|
|
@@ -298,6 +362,7 @@ module SideJob
|
|
298
362
|
# @param token [String] Token returned by {#lock}
|
299
363
|
# @return [Boolean] Whether the job was unlocked
|
300
364
|
def unlock(token)
|
365
|
+
check_exists
|
301
366
|
return SideJob.redis.eval('
|
302
367
|
if redis.call("get",KEYS[1]) == ARGV[1] then
|
303
368
|
return redis.call("del",KEYS[1])
|
@@ -306,6 +371,12 @@ module SideJob
|
|
306
371
|
end', { keys: ["#{redis_key}:lock"], argv: [token] }) == 1
|
307
372
|
end
|
308
373
|
|
374
|
+
# Publishes a message to the job's channel.
|
375
|
+
# @param message [Object] JSON encodable message
|
376
|
+
def publish(message)
|
377
|
+
SideJob.publish "/sidejob/job/#{id}", message
|
378
|
+
end
|
379
|
+
|
309
380
|
private
|
310
381
|
|
311
382
|
# Queue or schedule this job using sidekiq.
|
@@ -314,21 +385,17 @@ module SideJob
|
|
314
385
|
# Don't need to queue if a worker is already in process of running
|
315
386
|
return if SideJob.redis.exists "#{redis_key}:lock:worker"
|
316
387
|
|
317
|
-
|
318
|
-
|
388
|
+
worker = JSON.parse(SideJob.redis.get("#{redis_key}:worker"))
|
319
389
|
# Don't need to queue if the job is already in the queue (this does not include scheduled jobs)
|
320
390
|
# When Sidekiq pulls job out from scheduled set, we can still get the same job queued multiple times
|
321
391
|
# but the server middleware handles it
|
322
|
-
return if Sidekiq::Queue.new(queue).find_job(@id)
|
323
|
-
|
324
|
-
klass = get(:class)
|
325
|
-
args = get(:args)
|
392
|
+
return if Sidekiq::Queue.new(worker['queue']).find_job(@id)
|
326
393
|
|
327
|
-
if ! SideJob.
|
394
|
+
if ! SideJob::Worker.config(worker['queue'], worker['class'])
|
328
395
|
self.status = 'terminated'
|
329
|
-
raise "Worker no longer registered for #{klass} in queue #{queue}"
|
396
|
+
raise "Worker no longer registered for #{klass} in queue #{worker['queue']}"
|
330
397
|
end
|
331
|
-
item = {'jid' => id, 'queue' => queue, 'class' =>
|
398
|
+
item = {'jid' => id, 'queue' => worker['queue'], 'class' => worker['class'], 'args' => worker['args'] || [], 'retry' => false}
|
332
399
|
item['at'] = time if time && time > Time.now.to_f
|
333
400
|
Sidekiq::Client.push(item)
|
334
401
|
end
|
@@ -338,17 +405,24 @@ module SideJob
|
|
338
405
|
SideJob.redis.smembers("#{redis_key}:#{type}ports").reject {|name| name == '*'}.map {|name| SideJob::Port.new(self, type, name)}
|
339
406
|
end
|
340
407
|
|
408
|
+
# Return the worker configuration
|
409
|
+
# @return [Hash] Worker config for the job
|
410
|
+
def worker_config
|
411
|
+
worker = JSON.parse(SideJob.redis.get("#{redis_key}:worker"))
|
412
|
+
SideJob::Worker.config(worker['queue'], worker['class']) || {}
|
413
|
+
end
|
414
|
+
|
341
415
|
# Sets the input/outputs ports for the job and overwrites all current options.
|
342
416
|
# The ports are merged with the worker configuration.
|
343
417
|
# Any current ports that are not in the new port set are deleted (including any data on those ports).
|
344
418
|
# @param type [:in, :out] Input or output ports
|
345
419
|
# @param ports [Hash{Symbol,String => Hash}] Port configuration. Port name to options.
|
346
420
|
def set_ports(type, ports)
|
421
|
+
check_exists
|
347
422
|
current = SideJob.redis.smembers("#{redis_key}:#{type}ports") || []
|
348
|
-
config = SideJob::Worker.config(get(:queue), get(:class))
|
349
423
|
|
350
424
|
ports ||= {}
|
351
|
-
ports = (
|
425
|
+
ports = (worker_config["#{type}ports"] || {}).merge(ports.dup.stringify_keys)
|
352
426
|
ports.each_key do |port|
|
353
427
|
ports[port] = ports[port].stringify_keys
|
354
428
|
end
|
@@ -365,13 +439,27 @@ module SideJob
|
|
365
439
|
# replace port defaults
|
366
440
|
defaults = ports.map do |port, options|
|
367
441
|
if options.has_key?('default')
|
368
|
-
[port, options['default']
|
442
|
+
[port, SideJob::Port.encode_data(options['default'])]
|
369
443
|
else
|
370
444
|
nil
|
371
445
|
end
|
372
446
|
end.compact.flatten(1)
|
373
447
|
multi.del "#{redis_key}:#{type}ports:default"
|
374
448
|
multi.hmset "#{redis_key}:#{type}ports:default", *defaults if defaults.length > 0
|
449
|
+
|
450
|
+
# replace port channels
|
451
|
+
channels = ports.map do |port, options|
|
452
|
+
if options.has_key?('channels')
|
453
|
+
options['channels'].each do |channel|
|
454
|
+
multi.sadd "channel:#{channel}", id
|
455
|
+
end
|
456
|
+
[port, options['channels'].to_json]
|
457
|
+
else
|
458
|
+
nil
|
459
|
+
end
|
460
|
+
end.compact.flatten(1)
|
461
|
+
multi.del "#{redis_key}:#{type}ports:channels"
|
462
|
+
multi.hmset "#{redis_key}:#{type}ports:channels", *channels if channels.length > 0
|
375
463
|
end
|
376
464
|
end
|
377
465
|
|
@@ -386,9 +474,9 @@ module SideJob
|
|
386
474
|
class Job
|
387
475
|
include JobMethods
|
388
476
|
|
389
|
-
# @param
|
390
|
-
def initialize(
|
391
|
-
@id =
|
477
|
+
# @param alias_or_id [String, Integer] Job alias or id
|
478
|
+
def initialize(alias_or_id)
|
479
|
+
@id = (SideJob.redis.hget('jobs:aliases', alias_or_id.to_s) || alias_or_id).to_i
|
392
480
|
check_exists
|
393
481
|
end
|
394
482
|
end
|