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