sidejob 3.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +5 -0
- data/.rspec +2 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +61 -0
- data/README.md +128 -0
- data/bin/build +11 -0
- data/bin/console +7 -0
- data/lib/sidejob/job.rb +425 -0
- data/lib/sidejob/port.rb +206 -0
- data/lib/sidejob/server_middleware.rb +117 -0
- data/lib/sidejob/testing.rb +54 -0
- data/lib/sidejob/version.rb +4 -0
- data/lib/sidejob/worker.rb +133 -0
- data/lib/sidejob.rb +116 -0
- data/sidejob.gemspec +21 -0
- data/spec/integration/fib_spec.rb +51 -0
- data/spec/integration/sum_spec.rb +39 -0
- data/spec/sidejob/job_spec.rb +543 -0
- data/spec/sidejob/port_spec.rb +452 -0
- data/spec/sidejob/server_middleware_spec.rb +254 -0
- data/spec/sidejob/testing_spec.rb +108 -0
- data/spec/sidejob/worker_spec.rb +201 -0
- data/spec/sidejob_spec.rb +182 -0
- data/spec/spec_helper.rb +21 -0
- data/spec/support/test_sum.rb +17 -0
- data/spec/support/test_worker.rb +6 -0
- metadata +140 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: d993f3349b71794286776cd15dd1a08052466080
|
4
|
+
data.tar.gz: db8eb058a8329b4be54604762efaa68b7f053bff
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: ab477bb2a9ad4ad67977249c081a235c95c74e44cc4157e884d8570c4b6dde177d8f458142eff70323867706216c45329167441be46875d75676e0801c42e6c3
|
7
|
+
data.tar.gz: 94543337bb043a8c35a11ea0823419572d6bdf8c4d48011cc61d2c28e08ff9d2bb891d18d8e8cb10d430eb21d81d183284c5400d3e5288376aa6926fef2ee364
|
data/.rspec
ADDED
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,61 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
sidejob (3.0.0)
|
5
|
+
sidekiq (~> 3.2.5)
|
6
|
+
|
7
|
+
GEM
|
8
|
+
remote: https://rubygems.org/
|
9
|
+
specs:
|
10
|
+
celluloid (0.15.2)
|
11
|
+
timers (~> 1.1.0)
|
12
|
+
coderay (1.1.0)
|
13
|
+
connection_pool (2.0.0)
|
14
|
+
diff-lcs (1.2.5)
|
15
|
+
docile (1.1.5)
|
16
|
+
json (1.8.1)
|
17
|
+
method_source (0.8.2)
|
18
|
+
multi_json (1.10.1)
|
19
|
+
pry (0.9.12.6)
|
20
|
+
coderay (~> 1.0)
|
21
|
+
method_source (~> 0.8)
|
22
|
+
slop (~> 3.4)
|
23
|
+
redis (3.1.0)
|
24
|
+
redis-namespace (1.5.1)
|
25
|
+
redis (~> 3.0, >= 3.0.4)
|
26
|
+
rspec (3.1.0)
|
27
|
+
rspec-core (~> 3.1.0)
|
28
|
+
rspec-expectations (~> 3.1.0)
|
29
|
+
rspec-mocks (~> 3.1.0)
|
30
|
+
rspec-core (3.1.7)
|
31
|
+
rspec-support (~> 3.1.0)
|
32
|
+
rspec-expectations (3.1.2)
|
33
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
34
|
+
rspec-support (~> 3.1.0)
|
35
|
+
rspec-mocks (3.1.3)
|
36
|
+
rspec-support (~> 3.1.0)
|
37
|
+
rspec-support (3.1.2)
|
38
|
+
sidekiq (3.2.6)
|
39
|
+
celluloid (= 0.15.2)
|
40
|
+
connection_pool (>= 2.0.0)
|
41
|
+
json
|
42
|
+
redis (>= 3.0.6)
|
43
|
+
redis-namespace (>= 1.3.1)
|
44
|
+
simplecov (0.9.0)
|
45
|
+
docile (~> 1.1.0)
|
46
|
+
multi_json
|
47
|
+
simplecov-html (~> 0.8.0)
|
48
|
+
simplecov-html (0.8.0)
|
49
|
+
slop (3.4.7)
|
50
|
+
timers (1.1.0)
|
51
|
+
yard (0.8.7.4)
|
52
|
+
|
53
|
+
PLATFORMS
|
54
|
+
ruby
|
55
|
+
|
56
|
+
DEPENDENCIES
|
57
|
+
pry
|
58
|
+
rspec
|
59
|
+
sidejob!
|
60
|
+
simplecov
|
61
|
+
yard
|
data/README.md
ADDED
@@ -0,0 +1,128 @@
|
|
1
|
+
SideJob
|
2
|
+
=======
|
3
|
+
|
4
|
+
SideJob is built on top of [Sidekiq](https://github.com/mperham/sidekiq) and
|
5
|
+
[Redis](http://redis.io/). Sidekiq jobs typically complete relatively quickly.
|
6
|
+
Just like a typical side job, a job in SideJob may depend on other jobs or may make slow progress
|
7
|
+
and take a long time (such as months). They may be suspended and restarted many times.
|
8
|
+
The job should be robust to the crashing or downtime of any portion of the infrastructure.
|
9
|
+
|
10
|
+
Requirements
|
11
|
+
------------
|
12
|
+
|
13
|
+
Ruby 2.0 or greater and Redis 2.8 or greater is recommended.
|
14
|
+
|
15
|
+
Jobs
|
16
|
+
----
|
17
|
+
|
18
|
+
* Jobs have a unique ID assigned using incrementing numbers
|
19
|
+
* This ID is used as Sidekiq's jid
|
20
|
+
* Note: a job can be queued multiple times on Sidekiq's queues
|
21
|
+
* Therefore, Sidekiq's jids are not unique
|
22
|
+
* Jobs have a queue and class name
|
23
|
+
* Jobs have any number of input and output ports
|
24
|
+
* A job can have any number of named child jobs
|
25
|
+
* Each job has at most one parent job
|
26
|
+
* Jobs can store any JSON encoded object in its internal state
|
27
|
+
|
28
|
+
Jobs can have a number of different status. The statuses and possible status transitions:
|
29
|
+
|
30
|
+
* -> queued
|
31
|
+
* queued -> running | terminating
|
32
|
+
* running -> queued | suspended | completed | failed | terminating
|
33
|
+
* suspended | completed | failed -> queued | terminating
|
34
|
+
* terminating -> terminated
|
35
|
+
* terminated -> queued
|
36
|
+
|
37
|
+
The difference between suspended, completed, and failed is only in their implications on the
|
38
|
+
internal job state. Completed implies that the job has processed all data and can be naturally
|
39
|
+
terminated. If additional input arrives, a completed job could continue running. Suspended implies
|
40
|
+
that the job is waiting for some input. Failed means that an exception was thrown.
|
41
|
+
|
42
|
+
Jobs that have been terminated along with all their children can be deleted entirely.
|
43
|
+
|
44
|
+
Ports
|
45
|
+
-----
|
46
|
+
|
47
|
+
* Ports are named (case sensitive) and must match `/^[a-zA-Z0-9_]+$/`.
|
48
|
+
* Any object that can be JSON encoded can be written or read from any input or output port.
|
49
|
+
* Ports must be explicitly specified for each job either by the worker configuration or when queuing new jobs unless
|
50
|
+
a port named `*` exists in which case new ports are dynamically created and inherit its options.
|
51
|
+
|
52
|
+
Port options:
|
53
|
+
|
54
|
+
* mode
|
55
|
+
* Queue - This is the default operation mode. All data written is read in a first in first out manner.
|
56
|
+
* Memory - No data is stored on the port. The most recent value sets the port default value.
|
57
|
+
* default - Default value when a read is done on the port with no data
|
58
|
+
|
59
|
+
Workers
|
60
|
+
-------
|
61
|
+
|
62
|
+
* A worker is the implementation of a specific job class
|
63
|
+
* Workers are required to register themselves
|
64
|
+
* A Sidekiq process should only handle a single queue so all registered workers in the process are for the same queue
|
65
|
+
* It should have a perform method that is called on each run
|
66
|
+
* It may have a shutdown method that is called before the job is terminated
|
67
|
+
* Workers should be idempotent as they may be run more than once for the same state
|
68
|
+
* SideJob ensures only one worker thread runs for a given job at a time
|
69
|
+
* Workers are responsible for managing state across runs
|
70
|
+
* Workers can suspend themselves when waiting for inputs
|
71
|
+
|
72
|
+
Data Structure
|
73
|
+
--------------
|
74
|
+
|
75
|
+
SideJob uses Redis for all job processing and storage. Code using
|
76
|
+
SideJob should use API functions instead of accessing redis directly,
|
77
|
+
but this is a description of the current data storage format.
|
78
|
+
|
79
|
+
The easiest way to set the redis location is via the environment
|
80
|
+
variable SIDEJOB_URL, e.g. redis://redis.myhost.com:6379/4
|
81
|
+
|
82
|
+
The keys used by Sidekiq:
|
83
|
+
|
84
|
+
* queues - Set containing all queue names
|
85
|
+
* queue:<queue> - List containing jobs to be run (new jobs pushed on left) on the given queue
|
86
|
+
* A sidekiq job is encoded as json and contains at minimum: queue, retry, class, jid, args
|
87
|
+
* schedule - Sorted set by schedule time of jobs to run in the future
|
88
|
+
* retry - Sorted set by retry time of jobs to retry
|
89
|
+
* dead - Sorted set by addition time of dead jobs
|
90
|
+
* processes - Set of sidekiq processes (values are host:pid)
|
91
|
+
* <host>:<pid> - Hash representing a connected sidekiq process
|
92
|
+
* beat - heartbeat timestamp (every 5 seconds)
|
93
|
+
* busy - number of busy workers
|
94
|
+
* info - JSON encoded info with keys hostname, started_at, pid, tag, concurrency, queues, labels. Expiry of 60 seconds.
|
95
|
+
* <host>:<pid>:workers - Hash containing running workers (thread ID -> { queue: <queue>, payload: <message>, run_at: <timestamp> })
|
96
|
+
* <host>:<pid>-signals - List for remotely sending signals to a sidekiq process (USR1 and TERM), 60 second expiry.
|
97
|
+
* stat:processed - Cumulative number of jobs processed
|
98
|
+
* stat:failed - Cumulative number of jobs failed
|
99
|
+
* stat:processed:<date> - Number of jobs processed for given date
|
100
|
+
* stat:failed:<date> - Number of jobs failed for given date
|
101
|
+
|
102
|
+
Additional keys used by SideJob:
|
103
|
+
|
104
|
+
* workers:<queue> - Hash mapping class name to worker configuration. A worker should define
|
105
|
+
the inports and outports hashes that map port names to port options.
|
106
|
+
* jobs:last_id - Stores the last job ID (we use incrementing integers from 1)
|
107
|
+
* jobs:logs - List with JSON encoded logs.
|
108
|
+
* { timestamp: <date>, job: <id>, read: [{ job: <id>, <in|out>port: <port>, data: [<data>] }, ...], write: [{ job: <id>, <in|out>port: <port>, data: [<data>] }, ...] }
|
109
|
+
* { timestamp: <date>, job: <id>, error: <message>, backtrace: <exception backtrace> }
|
110
|
+
* jobs - Hash mapping active job IDs to JSON encoded job state.
|
111
|
+
* queue - queue name
|
112
|
+
* class - name of class
|
113
|
+
* args - array of arguments passed to worker's perform method
|
114
|
+
* created_at - timestamp that the job was first queued
|
115
|
+
* created_by - string indicating the entity that created the job. SideJob uses job:<id> for jobs created by another job.
|
116
|
+
* ran_at - timestamp of the start of the last run
|
117
|
+
* Any additional keys used by the worker to track internal state
|
118
|
+
* job:<id>:status - Job status as string.
|
119
|
+
* job:<id>:in:<inport> and job:<id>:out:<outport> - List with unread port data. New data is pushed on the right.
|
120
|
+
* job:<id>:inports:mode and job:<id>:outports:mode - Hash mapping port name to port mode. All existing ports must be here.
|
121
|
+
* job:<id>:inports:default and job:<id>:outports:default - Hash mapping port name to JSON encoded default value for port.
|
122
|
+
* job:<id>:ancestors - List with parent job IDs up to the root job that has no parent.
|
123
|
+
Newer jobs are pushed on the left so the immediate parent is on the left and the root job is on the right.
|
124
|
+
* job:<id>:children - Hash mapping child job name to child job ID
|
125
|
+
* job:<id>:rate:<timestamp> - Rate limiter used to prevent run away executing of a job.
|
126
|
+
Keys are automatically expired.
|
127
|
+
* job:<id>:lock - Used to prevent multiple worker threads from running a job.
|
128
|
+
Auto expired to prevent stale locks.
|
data/bin/build
ADDED
data/bin/console
ADDED
data/lib/sidejob/job.rb
ADDED
@@ -0,0 +1,425 @@
|
|
1
|
+
module SideJob
|
2
|
+
# Methods shared between {SideJob::Job} and {SideJob::Worker}.
|
3
|
+
module JobMethods
|
4
|
+
attr_reader :id
|
5
|
+
attr_accessor :logger
|
6
|
+
|
7
|
+
# @return [Boolean] True if two jobs or workers have the same id
|
8
|
+
def ==(other)
|
9
|
+
other.respond_to?(:id) && id == other.id
|
10
|
+
end
|
11
|
+
|
12
|
+
# @see #==
|
13
|
+
def eql?(other)
|
14
|
+
self == other
|
15
|
+
end
|
16
|
+
|
17
|
+
# @return [Fixnum] Hash value based on the id
|
18
|
+
def hash
|
19
|
+
id.hash
|
20
|
+
end
|
21
|
+
|
22
|
+
# @return [String] Prefix for all redis keys related to this job
|
23
|
+
def redis_key
|
24
|
+
"job:#{id}"
|
25
|
+
end
|
26
|
+
alias :to_s :redis_key
|
27
|
+
|
28
|
+
# Returns if the job still exists.
|
29
|
+
# @return [Boolean] Returns true if this job exists and has not been deleted
|
30
|
+
def exists?
|
31
|
+
SideJob.redis.hexists 'jobs', id
|
32
|
+
end
|
33
|
+
|
34
|
+
# If a job logger is defined, call the log method on it with the log entry. Otherwise, call {SideJob.log}.
|
35
|
+
# @param entry [Hash] Log entry
|
36
|
+
def log(entry)
|
37
|
+
entry[:job] = id unless entry[:job]
|
38
|
+
(@logger || SideJob).log(entry)
|
39
|
+
end
|
40
|
+
|
41
|
+
# Groups all port reads and writes within the block into a single logged event.
|
42
|
+
# @param metadata [Hash] If provided, the metadata is merged into the final log entry
|
43
|
+
def group_port_logs(metadata={}, &block)
|
44
|
+
new_group = @logger.nil?
|
45
|
+
@logger ||= GroupPortLogs.new(self)
|
46
|
+
@logger.add_metadata metadata
|
47
|
+
yield
|
48
|
+
ensure
|
49
|
+
if new_group
|
50
|
+
@logger.done
|
51
|
+
@logger = nil
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
# Retrieve the job's status.
|
56
|
+
# @return [String] Job status
|
57
|
+
def status
|
58
|
+
SideJob.redis.get "#{redis_key}:status"
|
59
|
+
end
|
60
|
+
|
61
|
+
# Set the job status.
|
62
|
+
# @param status [String] The new job status
|
63
|
+
def status=(status)
|
64
|
+
SideJob.redis.set "#{redis_key}:status", status
|
65
|
+
end
|
66
|
+
|
67
|
+
# Prepare to terminate the job. Sets status to 'terminating'.
|
68
|
+
# Then queues the job so that its shutdown method if it exists can be run.
|
69
|
+
# After shutdown, the status will be 'terminated'.
|
70
|
+
# If the job is currently running, it will finish running first.
|
71
|
+
# If the job is already terminated, it does nothing.
|
72
|
+
# To start the job after termination, call {#run} with force: true.
|
73
|
+
# @param recursive [Boolean] If true, recursively terminate all children (default false)
|
74
|
+
# @return [SideJob::Job] self
|
75
|
+
def terminate(recursive: false)
|
76
|
+
if status != 'terminated'
|
77
|
+
self.status = 'terminating'
|
78
|
+
sidekiq_queue
|
79
|
+
end
|
80
|
+
if recursive
|
81
|
+
children.each_value do |child|
|
82
|
+
child.terminate(recursive: true)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
self
|
86
|
+
end
|
87
|
+
|
88
|
+
# Run the job.
|
89
|
+
# This method ensures that the job runs at least once from the beginning.
|
90
|
+
# If the job is currently running, it will run again.
|
91
|
+
# Just like sidekiq, we make no guarantees that the job will not be run more than once.
|
92
|
+
# Unless force is set, if the status is terminating or terminated, the job will not be run.
|
93
|
+
# @param force [Boolean] Whether to run if job is terminated (default false)
|
94
|
+
# @param at [Time, Float] Time to schedule the job, otherwise queue immediately
|
95
|
+
# @param wait [Float] Run in the specified number of seconds
|
96
|
+
# @return [SideJob::Job] self
|
97
|
+
def run(force: false, at: nil, wait: nil)
|
98
|
+
check_exists
|
99
|
+
|
100
|
+
case status
|
101
|
+
when 'terminating', 'terminated'
|
102
|
+
return unless force
|
103
|
+
end
|
104
|
+
|
105
|
+
self.status = 'queued'
|
106
|
+
|
107
|
+
time = nil
|
108
|
+
if at
|
109
|
+
time = at
|
110
|
+
time = time.to_f if time.is_a?(Time)
|
111
|
+
elsif wait
|
112
|
+
time = Time.now.to_f + wait
|
113
|
+
end
|
114
|
+
sidekiq_queue(time)
|
115
|
+
|
116
|
+
self
|
117
|
+
end
|
118
|
+
|
119
|
+
# Returns a child job by name.
|
120
|
+
# @param name [Symbol, String] Child job name to look up
|
121
|
+
# @return [SideJob::Job, nil] Child job or nil if not found
|
122
|
+
def child(name)
|
123
|
+
SideJob.find(SideJob.redis.hget("#{redis_key}:children", name))
|
124
|
+
end
|
125
|
+
|
126
|
+
# Returns all children jobs.
|
127
|
+
# @return [Hash<String => SideJob::Job>] Children jobs by name
|
128
|
+
def children
|
129
|
+
SideJob.redis.hgetall("#{redis_key}:children").each_with_object({}) {|child, hash| hash[child[0]] = SideJob.find(child[1])}
|
130
|
+
end
|
131
|
+
|
132
|
+
# Returns all ancestor jobs.
|
133
|
+
# @return [Array<SideJob::Job>] Ancestors (parent will be first and root job will be last)
|
134
|
+
def ancestors
|
135
|
+
SideJob.redis.lrange("#{redis_key}:ancestors", 0, -1).map { |id| SideJob.find(id) }
|
136
|
+
end
|
137
|
+
|
138
|
+
# Returns the parent job.
|
139
|
+
# @return [SideJob::Job, nil] Parent job or nil if none
|
140
|
+
def parent
|
141
|
+
parent = SideJob.redis.lindex("#{redis_key}:ancestors", 0)
|
142
|
+
parent = SideJob.find(parent) if parent
|
143
|
+
parent
|
144
|
+
end
|
145
|
+
|
146
|
+
# Returns if job and all children are terminated.
|
147
|
+
# @return [Boolean] True if this job and all children recursively are terminated
|
148
|
+
def terminated?
|
149
|
+
return false if status != 'terminated'
|
150
|
+
children.each_value do |child|
|
151
|
+
return false unless child.terminated?
|
152
|
+
end
|
153
|
+
return true
|
154
|
+
end
|
155
|
+
|
156
|
+
# Deletes the job and all children jobs (recursively) if all are terminated.
|
157
|
+
# @return [Boolean] Whether the job was deleted
|
158
|
+
def delete
|
159
|
+
return false unless terminated?
|
160
|
+
|
161
|
+
# recursively delete all children first
|
162
|
+
children.each_value do |child|
|
163
|
+
child.delete
|
164
|
+
end
|
165
|
+
|
166
|
+
# delete all SideJob keys
|
167
|
+
ports = inports.map(&:redis_key) + outports.map(&:redis_key)
|
168
|
+
SideJob.redis.multi do |multi|
|
169
|
+
multi.hdel 'jobs', id
|
170
|
+
multi.del ports + %w{status children ancestors inports:mode outports:mode inports:default outports:default}.map {|x| "#{redis_key}:#{x}" }
|
171
|
+
end
|
172
|
+
reload
|
173
|
+
return true
|
174
|
+
end
|
175
|
+
|
176
|
+
# Returns an input port.
|
177
|
+
# @param name [Symbol,String] Name of the port
|
178
|
+
# @return [SideJob::Port]
|
179
|
+
# @raise [RuntimeError] Error raised if port does not exist
|
180
|
+
def input(name)
|
181
|
+
get_port :in, name
|
182
|
+
end
|
183
|
+
|
184
|
+
# Returns an output port
|
185
|
+
# @param name [Symbol,String] Name of the port
|
186
|
+
# @return [SideJob::Port]
|
187
|
+
# @raise [RuntimeError] Error raised if port does not exist
|
188
|
+
def output(name)
|
189
|
+
get_port :out, name
|
190
|
+
end
|
191
|
+
|
192
|
+
# Gets all input ports.
|
193
|
+
# @return [Array<SideJob::Port>] Input ports
|
194
|
+
def inports
|
195
|
+
load_ports if ! @ports
|
196
|
+
@ports[:in].values
|
197
|
+
end
|
198
|
+
|
199
|
+
# Sets the input ports for the job.
|
200
|
+
# The ports are merged with the worker configuration.
|
201
|
+
# Any current ports that are not in the new port set are deleted (including any data on those ports).
|
202
|
+
# @param ports [Hash{Symbol,String => Hash}] Input port configuration. Port name to options.
|
203
|
+
def inports=(ports)
|
204
|
+
set_ports :in, ports
|
205
|
+
end
|
206
|
+
|
207
|
+
# Gets all output ports.
|
208
|
+
# @return [Array<SideJob::Port>] Output ports
|
209
|
+
def outports
|
210
|
+
load_ports if ! @ports
|
211
|
+
@ports[:out].values
|
212
|
+
end
|
213
|
+
|
214
|
+
# Sets the input ports for the job.
|
215
|
+
# The ports are merged with the worker configuration.
|
216
|
+
# Any current ports that are not in the new port set are deleted (including any data on those ports).
|
217
|
+
# @param ports [Hash{Symbol,String => Hash}] Output port configuration. Port name to options.
|
218
|
+
def outports=(ports)
|
219
|
+
set_ports :out, ports
|
220
|
+
end
|
221
|
+
|
222
|
+
# Returns some data from the job's state.
|
223
|
+
# The job state is cached for the lifetime of the job object. Call {#reload} if the state may have changed.
|
224
|
+
# @param key [Symbol,String] Retrieve value for the given key
|
225
|
+
# @return [Object,nil] Value from the job state or nil if key does not exist
|
226
|
+
# @raise [RuntimeError] Error raised if job no longer exists
|
227
|
+
def get(key)
|
228
|
+
load_state
|
229
|
+
@state[key.to_s]
|
230
|
+
end
|
231
|
+
|
232
|
+
# Clears the state and ports cache.
|
233
|
+
def reload
|
234
|
+
@state = nil
|
235
|
+
@ports = nil
|
236
|
+
@config = nil
|
237
|
+
end
|
238
|
+
|
239
|
+
# Returns the worker configuration for the job.
|
240
|
+
# @see SideJob::Worker.config
|
241
|
+
def config
|
242
|
+
@config ||= SideJob::Worker.config(get(:queue), get(:class))
|
243
|
+
end
|
244
|
+
|
245
|
+
private
|
246
|
+
|
247
|
+
# Queue or schedule this job using sidekiq.
|
248
|
+
# @param time [Time, Float, nil] Time to schedule the job if specified
|
249
|
+
def sidekiq_queue(time=nil)
|
250
|
+
queue = get(:queue)
|
251
|
+
klass = get(:class)
|
252
|
+
args = get(:args)
|
253
|
+
|
254
|
+
if ! SideJob.redis.hexists("workers:#{queue}", klass)
|
255
|
+
self.status = 'terminated'
|
256
|
+
raise "Worker no longer registered for #{klass} in queue #{queue}"
|
257
|
+
end
|
258
|
+
item = {'jid' => id, 'queue' => queue, 'class' => klass, 'args' => args || [], 'retry' => false}
|
259
|
+
item['at'] = time if time && time > Time.now.to_f
|
260
|
+
Sidekiq::Client.push(item)
|
261
|
+
end
|
262
|
+
|
263
|
+
# Caches all inports and outports.
|
264
|
+
def load_ports
|
265
|
+
@ports = {}
|
266
|
+
%i{in out}.each do |type|
|
267
|
+
@ports[type] = {}
|
268
|
+
SideJob.redis.hkeys("#{redis_key}:#{type}ports:mode").each do |name|
|
269
|
+
if name == '*'
|
270
|
+
@ports["#{type}*"] = SideJob::Port.new(self, type, name)
|
271
|
+
else
|
272
|
+
@ports[type][name] = SideJob::Port.new(self, type, name)
|
273
|
+
end
|
274
|
+
end
|
275
|
+
end
|
276
|
+
end
|
277
|
+
|
278
|
+
# Returns an input or output port.
|
279
|
+
# @param type [:in, :out] Input or output port
|
280
|
+
# @param name [Symbol,String] Name of the port
|
281
|
+
# @return [SideJob::Port]
|
282
|
+
def get_port(type, name)
|
283
|
+
load_ports if ! @ports
|
284
|
+
name = name.to_s
|
285
|
+
return @ports[type][name] if @ports[type][name]
|
286
|
+
|
287
|
+
if @ports["#{type}*"]
|
288
|
+
# create port with default port options for dynamic ports
|
289
|
+
port = SideJob::Port.new(self, type, name)
|
290
|
+
port.options = @ports["#{type}*"].options
|
291
|
+
@ports[type][name] = port
|
292
|
+
return port
|
293
|
+
else
|
294
|
+
raise "Unknown #{type}put port: #{name}"
|
295
|
+
end
|
296
|
+
end
|
297
|
+
|
298
|
+
# Sets the input/outputs ports for the job and overwrites all current options.
|
299
|
+
# The ports are merged with the worker configuration.
|
300
|
+
# Any current ports that are not in the new port set are deleted (including any data on those ports).
|
301
|
+
# @param type [:in, :out] Input or output ports
|
302
|
+
# @param ports [Hash{Symbol,String => Hash}] Port configuration. Port name to options.
|
303
|
+
def set_ports(type, ports)
|
304
|
+
current = SideJob.redis.hkeys("#{redis_key}:#{type}ports:mode") || []
|
305
|
+
|
306
|
+
replace_port_data = []
|
307
|
+
ports = (ports || {}).stringify_keys
|
308
|
+
ports = (config["#{type}ports"] || {}).merge(ports)
|
309
|
+
ports.each_key do |port|
|
310
|
+
ports[port] = ports[port].stringify_keys
|
311
|
+
replace_port_data << port if ports[port]['data']
|
312
|
+
end
|
313
|
+
|
314
|
+
SideJob.redis.multi do |multi|
|
315
|
+
# remove data from old ports
|
316
|
+
((current - ports.keys) | replace_port_data).each do |port|
|
317
|
+
multi.del "#{redis_key}:#{type}:#{port}"
|
318
|
+
end
|
319
|
+
|
320
|
+
# completely replace the mode and default keys
|
321
|
+
|
322
|
+
multi.del "#{redis_key}:#{type}ports:mode"
|
323
|
+
modes = ports.map do |port, options|
|
324
|
+
[port, options['mode'] || 'queue']
|
325
|
+
end.flatten(1)
|
326
|
+
multi.hmset "#{redis_key}:#{type}ports:mode", *modes if modes.length > 0
|
327
|
+
|
328
|
+
defaults = ports.map do |port, options|
|
329
|
+
if options.has_key?('default')
|
330
|
+
[port, options['default'].to_json]
|
331
|
+
else
|
332
|
+
nil
|
333
|
+
end
|
334
|
+
end.compact.flatten(1)
|
335
|
+
multi.del "#{redis_key}:#{type}ports:default"
|
336
|
+
multi.hmset "#{redis_key}:#{type}ports:default", *defaults if defaults.length > 0
|
337
|
+
end
|
338
|
+
|
339
|
+
@ports = nil
|
340
|
+
|
341
|
+
group_port_logs do
|
342
|
+
ports.each_pair do |port, options|
|
343
|
+
if options['data']
|
344
|
+
port = get_port(type, port)
|
345
|
+
options['data'].each do |x|
|
346
|
+
port.write x
|
347
|
+
end
|
348
|
+
end
|
349
|
+
end
|
350
|
+
end
|
351
|
+
end
|
352
|
+
|
353
|
+
# @raise [RuntimeError] Error raised if job no longer exists
|
354
|
+
def check_exists
|
355
|
+
raise "Job #{id} no longer exists!" unless exists?
|
356
|
+
end
|
357
|
+
|
358
|
+
def load_state
|
359
|
+
if ! @state
|
360
|
+
state = SideJob.redis.hget('jobs', id)
|
361
|
+
raise "Job #{id} no longer exists!" if ! state
|
362
|
+
@state = JSON.parse(state)
|
363
|
+
end
|
364
|
+
@state
|
365
|
+
end
|
366
|
+
end
|
367
|
+
|
368
|
+
# Wrapper for a job which may not be in progress unlike SideJob::Worker.
|
369
|
+
# @see SideJob::JobMethods
|
370
|
+
class Job
|
371
|
+
include JobMethods
|
372
|
+
|
373
|
+
# @param id [String] Job id
|
374
|
+
def initialize(id)
|
375
|
+
@id = id
|
376
|
+
end
|
377
|
+
end
|
378
|
+
|
379
|
+
# Logger that groups all port read/writes together.
|
380
|
+
# @see {JobMethods#group_port_logs}
|
381
|
+
class GroupPortLogs
|
382
|
+
def initialize(job)
|
383
|
+
@metadata = {job: job.id}
|
384
|
+
end
|
385
|
+
|
386
|
+
# If entry is not a port log, send it on to {SideJob.log}. Otherwise, collect the log until {#done} is called.
|
387
|
+
# @param entry [Hash] Log entry
|
388
|
+
def log(entry)
|
389
|
+
if entry[:read] && entry[:write]
|
390
|
+
# collect reads and writes by port and group data together
|
391
|
+
@port_events ||= {read: {}, write: {}} # {job: id, <in|out>port: port} -> data array
|
392
|
+
%i{read write}.each do |type|
|
393
|
+
entry[type].each do |event|
|
394
|
+
data = event.delete(:data)
|
395
|
+
@port_events[type][event] ||= []
|
396
|
+
@port_events[type][event].concat data
|
397
|
+
end
|
398
|
+
end
|
399
|
+
else
|
400
|
+
SideJob.log(entry)
|
401
|
+
end
|
402
|
+
end
|
403
|
+
|
404
|
+
# Merges the collected port read and writes and send logs to {SideJob.log}.
|
405
|
+
def done
|
406
|
+
return unless @port_events && (@port_events[:read].length > 0 || @port_events[:write].length > 0)
|
407
|
+
|
408
|
+
entry = {}
|
409
|
+
%i{read write}.each do |type|
|
410
|
+
entry[type] = @port_events[type].map do |port, data|
|
411
|
+
port.merge({data: data})
|
412
|
+
end
|
413
|
+
end
|
414
|
+
|
415
|
+
SideJob.log @metadata.merge(entry)
|
416
|
+
@port_events = nil
|
417
|
+
end
|
418
|
+
|
419
|
+
# Add metadata fields to the final log entry.
|
420
|
+
# @param metadata [Hash] Data to be merged with the existing metadata and final log entry
|
421
|
+
def add_metadata(metadata)
|
422
|
+
@metadata.merge!(metadata.symbolize_keys)
|
423
|
+
end
|
424
|
+
end
|
425
|
+
end
|