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.
@@ -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