job_boss 0.6.8 → 0.7.5

Sign up to get free protection for your applications and to get access to all the features.
data/README.markdown CHANGED
@@ -92,6 +92,14 @@ You can even define a block to provide updates on progress (the value which is p
92
92
  puts "We're now at #{progress * 100}%"
93
93
  end
94
94
 
95
+ Prioritization of jobs is also supported. If a particular batch is more important than others, you can specify a higher priority
96
+
97
+ batch = Batch.new(:priority => 3)
98
+
99
+ In practical terms, the priority represents the number of jobs which are pulled from the queue to be processed each cycle, so by wary of increasing your priority beyond your maximum number of employees. No job queue will suffer from resource starvation, but you can greatly decrease the performance of other queues by over-prioritizing one.
100
+
101
+ Also note that job_boss uses a prioritized round-robin approach to scheduling jobs, the priority for jobs is increased throughout the run of the job queue, providing an approximation of a first-come first-serve approach to reduce latency.
102
+
95
103
  For performance, it is recommended that you keep your jobs table clean scheduling execution of the `delete_jobs_before` command on the Job model, which will clean all jobs completed before the specified time:
96
104
 
97
105
  Job.delete_jobs_before(2.days.ago)
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.8'
7
+ s.version = '0.7.5'
8
8
  s.platform = Gem::Platform::RUBY
9
9
  s.authors = ["Brian Underwood"]
10
10
  s.email = ["ml+job_boss@semi-sentient.com"]
@@ -2,15 +2,15 @@ require 'active_support'
2
2
 
3
3
  module JobBoss
4
4
  class Batch
5
- attr_accessor :batch_id
5
+ attr_accessor :batch_id, :priority
6
6
 
7
7
  extend ActiveSupport::Memoizable
8
8
  # Used to queue jobs in a batch
9
9
  # Usage:
10
10
  # batch.queue.math.is_prime?(42)
11
- def queue
11
+ def queue(attributes = {})
12
12
  require 'job_boss/queuer'
13
- Queuer.new(:batch_id => @batch_id)
13
+ Queuer.new({:priority => @priority, :batch_id => @batch_id}.merge(attributes))
14
14
  end
15
15
  memoize :queue
16
16
 
@@ -23,8 +23,11 @@ module JobBoss
23
23
  queue.send(controller).send(action, *args)
24
24
  end
25
25
 
26
- def initialize(batch_id = nil)
27
- @batch_id = batch_id || Batch.generate_batch_id
26
+ def initialize(options = {})
27
+ options[:priority] ||= 1
28
+
29
+ @priority = options[:priority]
30
+ @batch_id = options[:batch_id] || Batch.generate_batch_id
28
31
  end
29
32
 
30
33
  # Returns ActiveRecord::Relation representing query for jobs in batch
data/lib/job_boss/boss.rb CHANGED
@@ -18,9 +18,9 @@ module JobBoss
18
18
  # Used to queue jobs
19
19
  # Usage:
20
20
  # Boss.queue.math.is_prime?(42)
21
- def queue
21
+ def queue(attributes = {})
22
22
  require 'job_boss/queuer'
23
- Queuer.new
23
+ Queuer.new(attributes)
24
24
  end
25
25
  memoize :queue
26
26
 
@@ -86,28 +86,45 @@ module JobBoss
86
86
  logger.info "Job Boss started"
87
87
  logger.info "Employee limit: #{Boss.config.employee_limit}"
88
88
 
89
+ jobs = []
89
90
  while true
90
- unless (children_count = available_employees) > 0 && Job.pending.count > 0
91
- sleep(config.sleep_interval)
92
- next
93
- end
91
+ available_employee_count = wait_for_available_employees
94
92
 
95
- # Go through each pending path / batch so that we don't get stuck just processing
96
- # long running jobs which would leave quicker jobs to suffocate
97
- Job.pending.select('DISTINCT path, batch_id').reorder(nil).each do |distinct_job|
98
- job = Job.pending.order('id').find_by_path_and_batch_id(distinct_job.path, distinct_job.batch_id)
99
- next if job.nil?
93
+ jobs = dequeue_jobs if jobs.empty?
100
94
 
95
+ [available_employee_count, jobs.size].min.times do
96
+ job = jobs.shift
101
97
  job.dispatch(self)
102
98
  @running_jobs << job
103
-
104
- children_count -= 1
105
- break unless children_count > 0
106
99
  end
107
100
 
108
101
  end
109
102
  end
110
103
 
104
+ # Waits until there is at least one available employee and then returns count
105
+ def wait_for_available_employees
106
+ until (employee_count = available_employees) > 0 && Job.pending.count > 0
107
+ sleep(config.sleep_interval)
108
+ end
109
+
110
+ employee_count
111
+ end
112
+
113
+ # Dequeues next set of jobs based on prioritized round robin algorithm
114
+ # Priority of a particular queue determines how many jobs get pulled from that queue each time we dequeue
115
+ # A priority adjustment is also done to give greater priority to sets of jobs which have been running longer
116
+ def dequeue_jobs
117
+ logger.info "Dequeuing jobs"
118
+ Job.pending.select('DISTINCT priority, path, batch_id').reorder(nil).collect do |distinct_job|
119
+ queue_scope = Job.where(:path => distinct_job.path, :batch_id => distinct_job.batch_id)
120
+
121
+ # Give queues which have are further along more priority to reduce latency
122
+ priority_adjustment = ((queue_scope.completed.count.to_f / queue_scope.count) * config.employee_limit).floor
123
+
124
+ queue_scope.pending.limit(distinct_job.priority + priority_adjustment)
125
+ end.flatten.sort_by(&:id)
126
+ end
127
+
111
128
  def stop
112
129
  logger.info "Stopping #{@running_jobs.size} running employees..."
113
130
 
data/lib/job_boss/job.rb CHANGED
@@ -55,7 +55,7 @@ module JobBoss
55
55
  end
56
56
 
57
57
  def batch
58
- self.batch_id && Batch.new(self.batch_id)
58
+ self.batch_id && Batch.new(:batch_id => self.batch_id, :priority => self.priority)
59
59
  end
60
60
 
61
61
  def result
@@ -75,6 +75,7 @@ module JobBoss
75
75
  self.result = nil
76
76
  self.completed_at = nil
77
77
  self.status = nil
78
+ self.error_class = nil
78
79
  self.error_message = nil
79
80
  self.error_backtrace = nil
80
81
  self.employee_host = nil
@@ -1,6 +1,7 @@
1
1
  module JobBoss
2
2
  class Queuer
3
3
  def initialize(attributes = nil)
4
+ attributes[:priority] ||= 1
4
5
  @attributes = attributes || {}
5
6
  end
6
7
 
data/lib/migrate.rb CHANGED
@@ -11,6 +11,8 @@ class CreateJobs < ActiveRecord::Migration
11
11
  t.datetime :completed_at
12
12
  t.string :status
13
13
 
14
+ t.integer :priority, :default => 1, :null => false
15
+
14
16
  t.string :error_class
15
17
  t.string :error_message
16
18
  t.text :error_backtrace
@@ -187,21 +187,78 @@ class DaemonTest < ActiveSupport::TestCase
187
187
 
188
188
 
189
189
  first_jobs = (0..3).collect do
190
- Boss.queue.sleep.sleep_for(3)
190
+ Boss.queue.sleep.sleep_for(4)
191
191
  end
192
- job2 = Boss.queue.sleep.sleep_for(3)
192
+ job2 = Boss.queue.sleep.sleep_for(4)
193
193
 
194
- sleep(1)
194
+ sleep(2)
195
195
 
196
196
  assert first_jobs.all? {|job| job.running? }
197
197
  assert !job2.running?
198
- sleep(3)
198
+ sleep(4)
199
199
  assert first_jobs.all? {|job| !job.running? }
200
200
  assert job2.running?
201
- sleep(3)
201
+ sleep(4)
202
202
  assert first_jobs.all? {|job| !job.running? }
203
203
  assert !job2.running?
204
204
 
205
+
206
+
207
+ # Testing queue prioritization
208
+ first_batch = Batch.new
209
+ first_jobs = (0..2).collect do
210
+ first_batch.queue.sleep.sleep_for(4)
211
+ end
212
+ second_batch = Batch.new(:priority => 3)
213
+ second_jobs = (0..2).collect do
214
+ second_batch.queue.sleep.sleep_for(4)
215
+ end
216
+
217
+ sleep(2)
218
+
219
+ assert first_jobs.first.running?
220
+ assert first_jobs[1..-1].all? {|job| !job.running? }
221
+ assert second_jobs.all? {|job| job.running? }
222
+
223
+ sleep(4)
224
+
225
+ assert !first_jobs.first.running?
226
+ assert first_jobs[1..-1].all? {|job| job.running? }
227
+ assert second_jobs.all? {|job| !job.running? }
228
+
229
+ sleep(4)
230
+
231
+
232
+
233
+
234
+ # Testing adjusted priority which prioritizes jobs which are further along
235
+ first_batch = Batch.new
236
+ first_jobs = (0..7).collect do
237
+ first_batch.queue.sleep.sleep_for(4)
238
+ end
239
+
240
+ sleep(4)
241
+
242
+ assert first_jobs[0,4].all? {|job| job.running? }
243
+ assert first_jobs[4,4].all? {|job| !job.running? }
244
+
245
+ second_batch = Batch.new
246
+ second_jobs = (0..3).collect do
247
+ second_batch.queue.sleep.sleep_for(4)
248
+ end
249
+
250
+ sleep(2)
251
+
252
+ assert first_jobs[0,4].all? {|job| !job.running? }
253
+ assert first_jobs[4,3].all? {|job| job.running? } # First queue gets an extra employee since it's halfway done
254
+ assert !first_jobs.last.running?
255
+ assert second_jobs.first.running?
256
+ assert second_jobs[1,3].all? {|job| !job.running? }
257
+
258
+
259
+
260
+
261
+
205
262
  stop_daemon
206
263
  end
207
264
  end
metadata CHANGED
@@ -1,13 +1,12 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: job_boss
3
3
  version: !ruby/object:Gem::Version
4
- hash: 23
5
4
  prerelease: false
6
5
  segments:
7
6
  - 0
8
- - 6
9
- - 8
10
- version: 0.6.8
7
+ - 7
8
+ - 5
9
+ version: 0.7.5
11
10
  platform: ruby
12
11
  authors:
13
12
  - Brian Underwood
@@ -15,7 +14,7 @@ autorequire:
15
14
  bindir: bin
16
15
  cert_chain: []
17
16
 
18
- date: 2011-01-14 00:00:00 -05:00
17
+ date: 2011-02-27 00:00:00 -05:00
19
18
  default_executable: job_boss
20
19
  dependencies:
21
20
  - !ruby/object:Gem::Dependency
@@ -26,7 +25,6 @@ dependencies:
26
25
  requirements:
27
26
  - - ">="
28
27
  - !ruby/object:Gem::Version
29
- hash: 3
30
28
  segments:
31
29
  - 0
32
30
  version: "0"
@@ -40,7 +38,6 @@ dependencies:
40
38
  requirements:
41
39
  - - ">="
42
40
  - !ruby/object:Gem::Version
43
- hash: 3
44
41
  segments:
45
42
  - 0
46
43
  version: "0"
@@ -54,7 +51,6 @@ dependencies:
54
51
  requirements:
55
52
  - - ">="
56
53
  - !ruby/object:Gem::Version
57
- hash: 3
58
54
  segments:
59
55
  - 0
60
56
  version: "0"
@@ -172,7 +168,6 @@ required_ruby_version: !ruby/object:Gem::Requirement
172
168
  requirements:
173
169
  - - ">="
174
170
  - !ruby/object:Gem::Version
175
- hash: 3
176
171
  segments:
177
172
  - 0
178
173
  version: "0"
@@ -181,7 +176,6 @@ required_rubygems_version: !ruby/object:Gem::Requirement
181
176
  requirements:
182
177
  - - ">="
183
178
  - !ruby/object:Gem::Version
184
- hash: 23
185
179
  segments:
186
180
  - 1
187
181
  - 3