job_boss 0.6.0 → 0.6.5
Sign up to get free protection for your applications and to get access to all the features.
- 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
|