sidejob 3.0.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 +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
|