job_boss 0.6.0 → 0.6.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/CHANGELOG.markdown +10 -0
- data/README.markdown +7 -5
- data/job_boss.gemspec +1 -1
- data/lib/job_boss.rb +2 -0
- data/lib/job_boss/batch.rb +44 -0
- data/lib/job_boss/boss.rb +3 -3
- data/lib/job_boss/job.rb +32 -8
- data/lib/job_boss/queuer.rb +6 -2
- data/lib/migrate.rb +3 -0
- data/test/unit/job_test.rb +62 -0
- metadata +11 -3
data/CHANGELOG.markdown
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
## 0.6.5
|
2
|
+
|
3
|
+
* Ability to create jobs in batches. Batches allow for better management of jobs. Batches for the same task will also run in parallel as opposed to serially
|
4
|
+
* Can now call methods on a batch object that one can call as Job class methods. Examples: wait_for_jobs, result_hash, time_taken, completed_percent
|
5
|
+
|
6
|
+
## 0.6.0
|
7
|
+
|
8
|
+
* Concept of MIA jobs (boss process marks jobs as MIA when they are killed or otherwise die unhandled)
|
9
|
+
* Job.wait_for_jobs method takes a block which allows updating of progress percentage
|
10
|
+
* Output in jobs for MIA and cancelled jobs
|
data/README.markdown
CHANGED
@@ -67,26 +67,28 @@ But since you don't want to do that right now, it looks something like this:
|
|
67
67
|
From your Rails code or in a console:
|
68
68
|
|
69
69
|
require 'job_boss'
|
70
|
+
batch = Batch.new
|
70
71
|
jobs = (0..1000).collect do |i|
|
71
|
-
|
72
|
+
batch.queue.math.is_prime?(i)
|
72
73
|
end
|
73
74
|
|
74
75
|
Or:
|
75
76
|
|
76
77
|
jobs = []
|
78
|
+
batch = Batch.new
|
77
79
|
Article.select('id').find_in_batches(:batch_size => 10) do |articles|
|
78
|
-
jobs <<
|
80
|
+
jobs << batch.queue.article.refresh_cache(articles.collect(&:id))
|
79
81
|
end
|
80
82
|
|
81
83
|
job_boss also makes it easy to wait for the jobs to be done and to collect the results into a hash:
|
82
84
|
|
83
|
-
|
85
|
+
batch.wait_for_jobs # Will sleep until the jobs are all complete
|
84
86
|
|
85
|
-
|
87
|
+
batch.result_hash # => {[0]=>false, [1]=>false, [2]=>true, [3]=>true, [4]=>false, ... }
|
86
88
|
|
87
89
|
You can even define a block to provide updates on progress (the value which is passed into the block is a float between 0.0 and 100.0):
|
88
90
|
|
89
|
-
|
91
|
+
batch.wait_for_jobs do |progress|
|
90
92
|
puts "We're now at #{progress}%"
|
91
93
|
end
|
92
94
|
|
data/job_boss.gemspec
CHANGED
data/lib/job_boss.rb
CHANGED
@@ -0,0 +1,44 @@
|
|
1
|
+
require 'active_support'
|
2
|
+
|
3
|
+
module JobBoss
|
4
|
+
class Batch
|
5
|
+
attr_accessor :batch_id
|
6
|
+
|
7
|
+
extend ActiveSupport::Memoizable
|
8
|
+
# Used to queue jobs in a batch
|
9
|
+
# Usage:
|
10
|
+
# batch.queue.math.is_prime?(42)
|
11
|
+
def queue
|
12
|
+
require 'job_boss/queuer'
|
13
|
+
Queuer.new(:batch_id => @batch_id)
|
14
|
+
end
|
15
|
+
memoize :queue
|
16
|
+
|
17
|
+
def initialize(batch_id = nil)
|
18
|
+
@batch_id = batch_id || Batch.generate_batch_id
|
19
|
+
end
|
20
|
+
|
21
|
+
# Returns ActiveRecord::Relation representing query for jobs in batch
|
22
|
+
def jobs
|
23
|
+
Job.where('batch_id = ?', @batch_id)
|
24
|
+
end
|
25
|
+
|
26
|
+
# Allow calling of Job class methods from a batch which will be called
|
27
|
+
# on in the scope of the jobs for the batch
|
28
|
+
# Examples: wait_for_jobs, result_hash, time_taken, completed_percent
|
29
|
+
def method_missing(sym, *args, &block)
|
30
|
+
jobs.send(sym, *args, &block)
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
class << self
|
35
|
+
def generate_batch_id(size = 32)
|
36
|
+
characters = (0..9).to_a + ('a'..'f').to_a
|
37
|
+
|
38
|
+
(1..size).collect do |i|
|
39
|
+
characters[rand(characters.size)]
|
40
|
+
end.join
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
data/lib/job_boss/boss.rb
CHANGED
@@ -92,10 +92,10 @@ module JobBoss
|
|
92
92
|
next
|
93
93
|
end
|
94
94
|
|
95
|
-
# Go through each pending path so that we don't get stuck just processing
|
95
|
+
# Go through each pending path / batch so that we don't get stuck just processing
|
96
96
|
# long running jobs which would leave quicker jobs to suffocate
|
97
|
-
Job.
|
98
|
-
job = Job.pending.
|
97
|
+
Job.pending.select('DISTINCT path, batch_id').each do |distinct_job|
|
98
|
+
job = Job.pending.order('id').find_by_path_and_batch_id(distinct_job.path, distinct_job.batch_id)
|
99
99
|
next if job.nil?
|
100
100
|
|
101
101
|
job.dispatch(self)
|
data/lib/job_boss/job.rb
CHANGED
@@ -13,7 +13,7 @@ module JobBoss
|
|
13
13
|
scope :mia, where("completed_at IS NOT NULL AND status = 'mia'")
|
14
14
|
|
15
15
|
def prototype
|
16
|
-
self.path + "(#{self.args.join(', ')})"
|
16
|
+
self.path + "(#{self.args.collect(&:inspect).join(', ')})"
|
17
17
|
end
|
18
18
|
|
19
19
|
# Method used by the boss to dispatch an employee
|
@@ -54,6 +54,10 @@ module JobBoss
|
|
54
54
|
write_attribute(:result, [value])
|
55
55
|
end
|
56
56
|
|
57
|
+
def batch
|
58
|
+
self.batch_id && Batch.new(self.batch_id)
|
59
|
+
end
|
60
|
+
|
57
61
|
def result
|
58
62
|
# If the result is being called for but the job hasn't been completed, reload
|
59
63
|
# to check to see if there was a result
|
@@ -118,6 +122,15 @@ module JobBoss
|
|
118
122
|
employee_pid && employee_host
|
119
123
|
end
|
120
124
|
|
125
|
+
# Is the job running?
|
126
|
+
def running?
|
127
|
+
# If the #running? method is being called for but the job hasn't started, reload
|
128
|
+
# to check to see if it has been assigned
|
129
|
+
self.reload if started_at.nil? || completed_at.nil?
|
130
|
+
|
131
|
+
started_at && !completed_at
|
132
|
+
end
|
133
|
+
|
121
134
|
# How long did the job take?
|
122
135
|
def time_taken
|
123
136
|
# If the #time_taken method is being called for but the job doesn't seem to have started/completed
|
@@ -127,6 +140,19 @@ module JobBoss
|
|
127
140
|
completed_at - started_at if completed_at && started_at
|
128
141
|
end
|
129
142
|
|
143
|
+
# How long did have set of jobs taken?
|
144
|
+
# Returns nil if not all jobs are complete
|
145
|
+
def self.time_taken
|
146
|
+
return nil if self.completed.count != self.count
|
147
|
+
|
148
|
+
self.maximum(:completed_at) - self.minimum(:started_at)
|
149
|
+
end
|
150
|
+
|
151
|
+
# Returns the completion percentage of a set of jobs
|
152
|
+
def self.completed_percent
|
153
|
+
self.completed.count.to_f / self.count.to_f
|
154
|
+
end
|
155
|
+
|
130
156
|
# If the job raised an exception, this method will return the instance of that exception
|
131
157
|
# with the message and backtrace
|
132
158
|
def error
|
@@ -145,8 +171,9 @@ module JobBoss
|
|
145
171
|
# Given a job or an array of jobs
|
146
172
|
# Will cause the process to sleep until all specified jobs have completed
|
147
173
|
# sleep_interval specifies polling period
|
148
|
-
def wait_for_jobs(jobs, sleep_interval = 0.5)
|
174
|
+
def wait_for_jobs(jobs = nil, sleep_interval = 0.5)
|
149
175
|
jobs = [jobs] if jobs.is_a?(Job)
|
176
|
+
jobs = self.scoped if jobs.nil?
|
150
177
|
|
151
178
|
ids = jobs.collect(&:id)
|
152
179
|
Job.uncached do
|
@@ -154,7 +181,7 @@ module JobBoss
|
|
154
181
|
sleep(sleep_interval)
|
155
182
|
|
156
183
|
if block_given?
|
157
|
-
yield
|
184
|
+
yield((Job.where('id in (?)', ids).completed.count.to_f / jobs.size.to_f) * 100.0)
|
158
185
|
end
|
159
186
|
end
|
160
187
|
end
|
@@ -165,8 +192,9 @@ module JobBoss
|
|
165
192
|
# Given a job or an array of jobs
|
166
193
|
# Returns a hash where the keys are the job method arguments and the values are the
|
167
194
|
# results of the job processing
|
168
|
-
def result_hash(jobs)
|
195
|
+
def result_hash(jobs = nil)
|
169
196
|
jobs = [jobs] if jobs.is_a?(Job)
|
197
|
+
jobs = self.scoped if jobs.nil?
|
170
198
|
|
171
199
|
# the #result method automatically reloads the result here if needed but this will
|
172
200
|
# do it in one SQL call
|
@@ -237,10 +265,6 @@ private
|
|
237
265
|
|
238
266
|
controller_object.send(action, *args)
|
239
267
|
end
|
240
|
-
|
241
|
-
def pending_paths
|
242
|
-
self.pending.except(:order).select('DISTINCT path').collect(&:path)
|
243
|
-
end
|
244
268
|
end
|
245
269
|
end
|
246
270
|
end
|
data/lib/job_boss/queuer.rb
CHANGED
@@ -1,5 +1,9 @@
|
|
1
1
|
module JobBoss
|
2
2
|
class Queuer
|
3
|
+
def initialize(attributes = nil)
|
4
|
+
@attributes = attributes || {}
|
5
|
+
end
|
6
|
+
|
3
7
|
def method_missing(method_id, *args)
|
4
8
|
require 'active_support'
|
5
9
|
require 'job_boss/job'
|
@@ -17,8 +21,8 @@ module JobBoss
|
|
17
21
|
@class = nil
|
18
22
|
@controller = nil
|
19
23
|
|
20
|
-
Job.create(:path => path,
|
21
|
-
|
24
|
+
Job.create(@attributes.merge(:path => path,
|
25
|
+
:args => args))
|
22
26
|
else
|
23
27
|
raise ArgumentError, "Invalid action"
|
24
28
|
end
|
data/lib/migrate.rb
CHANGED
@@ -2,6 +2,8 @@ class CreateJobs < ActiveRecord::Migration
|
|
2
2
|
def self.up
|
3
3
|
create_table :jobs do |t|
|
4
4
|
t.string :path
|
5
|
+
t.string :batch_id
|
6
|
+
|
5
7
|
t.text :args
|
6
8
|
t.text :result
|
7
9
|
t.datetime :started_at
|
@@ -20,6 +22,7 @@ class CreateJobs < ActiveRecord::Migration
|
|
20
22
|
end
|
21
23
|
|
22
24
|
add_index :jobs, :path
|
25
|
+
add_index :jobs, :batch_id
|
23
26
|
add_index :jobs, :status
|
24
27
|
|
25
28
|
postgres = (ActiveRecord::Base.connection.adapter_name == 'PostgreSQL')
|
data/test/unit/job_test.rb
CHANGED
@@ -138,6 +138,68 @@ class DaemonTest < ActiveSupport::TestCase
|
|
138
138
|
assert_equal 7, job.result
|
139
139
|
|
140
140
|
|
141
|
+
# Test Batch class
|
142
|
+
batch = Batch.new
|
143
|
+
|
144
|
+
jobs = (0..10).collect do |i|
|
145
|
+
batch.queue.math.is_prime?(i)
|
146
|
+
end
|
147
|
+
assert_equal 0.0, batch.completed_percent
|
148
|
+
|
149
|
+
batch.wait_for_jobs
|
150
|
+
|
151
|
+
batch.result_hash.each do |args, result|
|
152
|
+
assert_equal MathJobs.new.is_prime?(args.first), result
|
153
|
+
end
|
154
|
+
|
155
|
+
assert_equal Job.result_hash(jobs), batch.result_hash
|
156
|
+
assert_equal 1.0, batch.completed_percent
|
157
|
+
assert batch.time_taken > 0.5
|
158
|
+
|
159
|
+
# Test to make sure that different batches run in parallel where non-batched jobs don't
|
160
|
+
|
161
|
+
batch1 = Batch.new
|
162
|
+
|
163
|
+
first_jobs = (0..3).collect do
|
164
|
+
batch1.queue.sleep.sleep_for(3)
|
165
|
+
end
|
166
|
+
|
167
|
+
batch2 = Batch.new
|
168
|
+
job2 = batch2.queue.sleep.sleep_for(3)
|
169
|
+
|
170
|
+
sleep(1)
|
171
|
+
|
172
|
+
assert first_jobs[0,3].all? {|job| job.running? }
|
173
|
+
assert !first_jobs.last.running?
|
174
|
+
assert job2.running?
|
175
|
+
|
176
|
+
sleep(3)
|
177
|
+
|
178
|
+
assert first_jobs[0,3].all? {|job| !job.running? }
|
179
|
+
assert first_jobs.last.running?
|
180
|
+
assert !job2.running?
|
181
|
+
|
182
|
+
sleep(3)
|
183
|
+
|
184
|
+
assert !first_jobs.last.running?
|
185
|
+
|
186
|
+
|
187
|
+
first_jobs = (0..3).collect do
|
188
|
+
Boss.queue.sleep.sleep_for(3)
|
189
|
+
end
|
190
|
+
job2 = Boss.queue.sleep.sleep_for(3)
|
191
|
+
|
192
|
+
sleep(1)
|
193
|
+
|
194
|
+
assert first_jobs.all? {|job| job.running? }
|
195
|
+
assert !job2.running?
|
196
|
+
sleep(3)
|
197
|
+
assert first_jobs.all? {|job| !job.running? }
|
198
|
+
assert job2.running?
|
199
|
+
sleep(3)
|
200
|
+
assert first_jobs.all? {|job| !job.running? }
|
201
|
+
assert !job2.running?
|
202
|
+
|
141
203
|
stop_daemon
|
142
204
|
end
|
143
205
|
end
|
metadata
CHANGED
@@ -1,12 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: job_boss
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
+
hash: 13
|
4
5
|
prerelease: false
|
5
6
|
segments:
|
6
7
|
- 0
|
7
8
|
- 6
|
8
|
-
-
|
9
|
-
version: 0.6.
|
9
|
+
- 5
|
10
|
+
version: 0.6.5
|
10
11
|
platform: ruby
|
11
12
|
authors:
|
12
13
|
- Brian Underwood
|
@@ -14,7 +15,7 @@ autorequire:
|
|
14
15
|
bindir: bin
|
15
16
|
cert_chain: []
|
16
17
|
|
17
|
-
date:
|
18
|
+
date: 2011-01-06 00:00:00 -05:00
|
18
19
|
default_executable: job_boss
|
19
20
|
dependencies:
|
20
21
|
- !ruby/object:Gem::Dependency
|
@@ -25,6 +26,7 @@ dependencies:
|
|
25
26
|
requirements:
|
26
27
|
- - ">="
|
27
28
|
- !ruby/object:Gem::Version
|
29
|
+
hash: 3
|
28
30
|
segments:
|
29
31
|
- 0
|
30
32
|
version: "0"
|
@@ -38,6 +40,7 @@ dependencies:
|
|
38
40
|
requirements:
|
39
41
|
- - ">="
|
40
42
|
- !ruby/object:Gem::Version
|
43
|
+
hash: 3
|
41
44
|
segments:
|
42
45
|
- 0
|
43
46
|
version: "0"
|
@@ -51,6 +54,7 @@ dependencies:
|
|
51
54
|
requirements:
|
52
55
|
- - ">="
|
53
56
|
- !ruby/object:Gem::Version
|
57
|
+
hash: 3
|
54
58
|
segments:
|
55
59
|
- 0
|
56
60
|
version: "0"
|
@@ -66,6 +70,7 @@ extensions: []
|
|
66
70
|
extra_rdoc_files: []
|
67
71
|
|
68
72
|
files:
|
73
|
+
- CHANGELOG.markdown
|
69
74
|
- Gemfile
|
70
75
|
- MIT-LICENSE
|
71
76
|
- README.markdown
|
@@ -134,6 +139,7 @@ files:
|
|
134
139
|
- init.rb
|
135
140
|
- job_boss.gemspec
|
136
141
|
- lib/job_boss.rb
|
142
|
+
- lib/job_boss/batch.rb
|
137
143
|
- lib/job_boss/boss.rb
|
138
144
|
- lib/job_boss/capistrano.rb
|
139
145
|
- lib/job_boss/config.rb
|
@@ -166,6 +172,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
166
172
|
requirements:
|
167
173
|
- - ">="
|
168
174
|
- !ruby/object:Gem::Version
|
175
|
+
hash: 3
|
169
176
|
segments:
|
170
177
|
- 0
|
171
178
|
version: "0"
|
@@ -174,6 +181,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
174
181
|
requirements:
|
175
182
|
- - ">="
|
176
183
|
- !ruby/object:Gem::Version
|
184
|
+
hash: 23
|
177
185
|
segments:
|
178
186
|
- 1
|
179
187
|
- 3
|