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.
@@ -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
- Boss.queue.math.is_prime?(i)
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 << Boss.queue.article.refresh_cache(articles.collect(&:id))
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
- Job.wait_for_jobs(jobs) # Will sleep until the jobs are all complete
85
+ batch.wait_for_jobs # Will sleep until the jobs are all complete
84
86
 
85
- Job.result_hash(jobs) # => {[0]=>false, [1]=>false, [2]=>true, [3]=>true, [4]=>false, ... }
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
- Job.wait_for_jobs(jobs) do |progress|
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
@@ -4,7 +4,7 @@ $:.unshift lib unless $:.include?(lib)
4
4
 
5
5
  Gem::Specification.new do |s|
6
6
  s.name = "job_boss"
7
- s.version = '0.6.0'
7
+ s.version = '0.6.5'
8
8
  s.platform = Gem::Platform::RUBY
9
9
  s.authors = ["Brian Underwood"]
10
10
  s.email = ["ml+job_boss@semi-sentient.com"]
data/lib/job_boss.rb CHANGED
@@ -1,9 +1,11 @@
1
1
  require 'job_boss/job'
2
2
  require 'job_boss/boss'
3
+ require 'job_boss/batch'
3
4
 
4
5
  module JobBoss
5
6
  autoload :Boss, 'job_boss/boss'
6
7
  autoload :Job, 'job_boss/job'
8
+ autoload :Batch, 'job_boss/batch'
7
9
  end
8
10
 
9
11
  include JobBoss
@@ -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.pending_paths.each do |path|
98
- job = Job.pending.find_by_path(path)
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 ((Job.where('id in (?)', ids).completed.count.to_f / jobs.size.to_f) * 100.0)
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
@@ -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
- :args => args)
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')
@@ -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
- - 0
9
- version: 0.6.0
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: 2010-12-22 00:00:00 -05:00
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