job_boss 0.2

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.
Files changed (47) hide show
  1. data/README.markdown +43 -0
  2. data/Rakefile +15 -0
  3. data/bin/job_boss +27 -0
  4. data/doc/ActiveRecord/Base.html +343 -0
  5. data/doc/ActiveRecord.html +185 -0
  6. data/doc/CreateJobs.html +256 -0
  7. data/doc/JobBoss/Boss.html +413 -0
  8. data/doc/JobBoss/Config.html +342 -0
  9. data/doc/JobBoss/Job.html +754 -0
  10. data/doc/JobBoss/Queuer.html +231 -0
  11. data/doc/JobBoss.html +480 -0
  12. data/doc/Mongrel/HttpServer.html +275 -0
  13. data/doc/Mongrel.html +185 -0
  14. data/doc/Passenger/Railz/RequestHandler.html +271 -0
  15. data/doc/Passenger/Railz.html +185 -0
  16. data/doc/Passenger.html +185 -0
  17. data/doc/PhusionPassenger/Rack/RequestHandler.html +271 -0
  18. data/doc/PhusionPassenger/Rack.html +185 -0
  19. data/doc/PhusionPassenger/Railz/RequestHandler.html +271 -0
  20. data/doc/PhusionPassenger/Railz.html +185 -0
  21. data/doc/PhusionPassenger.html +187 -0
  22. data/doc/Rakefile.html +115 -0
  23. data/doc/Spawn/SpawnId.html +276 -0
  24. data/doc/Spawn.html +742 -0
  25. data/doc/bin/job_boss.html +58 -0
  26. data/doc/created.rid +9 -0
  27. data/doc/index.html +126 -0
  28. data/doc/lib/job_boss/boss_rb.html +64 -0
  29. data/doc/lib/job_boss/capistrano_rb.html +60 -0
  30. data/doc/lib/job_boss/configuror_rb.html +54 -0
  31. data/doc/lib/job_boss/job_rb.html +56 -0
  32. data/doc/lib/job_boss/queuer_rb.html +56 -0
  33. data/doc/lib/migrate_rb.html +52 -0
  34. data/doc/rdoc.css +706 -0
  35. data/doc/vendor/spawn/CHANGELOG.html +275 -0
  36. data/doc/vendor/spawn/LICENSE.html +151 -0
  37. data/doc/vendor/spawn/init_rb.html +54 -0
  38. data/doc/vendor/spawn/lib/patches_rb.html +56 -0
  39. data/doc/vendor/spawn/lib/spawn_rb.html +52 -0
  40. data/job_boss.gemspec +26 -0
  41. data/lib/job_boss/boss.rb +153 -0
  42. data/lib/job_boss/capistrano.rb +8 -0
  43. data/lib/job_boss/configuror.rb +40 -0
  44. data/lib/job_boss/job.rb +155 -0
  45. data/lib/job_boss/queuer.rb +37 -0
  46. data/lib/migrate.rb +27 -0
  47. metadata +151 -0
@@ -0,0 +1,153 @@
1
+ module JobBoss
2
+ class Boss
3
+ class << self
4
+ # Used to set Boss configuration
5
+ # Usage:
6
+ # Boss.config.sleep_interval = 2
7
+ def config
8
+ require 'job_boss/config'
9
+ @@config ||= Config.new
10
+ end
11
+
12
+ # Used to queue jobs
13
+ # Usage:
14
+ # Boss.queue.math.is_prime?(42)
15
+ def queue
16
+ require 'job_boss/queuer'
17
+ @@queuer ||= Queuer.new
18
+ end
19
+ end
20
+
21
+ def initialize(options = {})
22
+ @@config.working_dir ||= options[:working_dir]
23
+ @@config.sleep_interval ||= options[:sleep_interval]
24
+ @@config.employee_limit ||= options[:employee_limit]
25
+ @@config.database_yaml_path ||= options[:database_yaml_path]
26
+ @@config.jobs_path ||= options[:jobs_path]
27
+
28
+ @running_jobs = []
29
+ end
30
+
31
+ # Start the boss
32
+ def start
33
+ require 'active_record'
34
+ require 'yaml'
35
+
36
+ establish_active_record_connection
37
+
38
+ require_job_classes
39
+
40
+ require 'job_boss/job'
41
+
42
+ migrate
43
+
44
+ Signal.trap("HUP") do
45
+ stop
46
+ end
47
+
48
+ at_exit do
49
+ stop if Process.pid == BOSS_PID
50
+ end
51
+
52
+ puts "Job Boss started"
53
+
54
+ while true
55
+ unless (children_count = available_employees) > 0 && Job.pending.count > 0
56
+ sleep(@@config.sleep_interval)
57
+ next
58
+ end
59
+
60
+ Job.pending_paths.each do |path|
61
+ job = Job.pending.find_by_path(path)
62
+ next if job.nil?
63
+
64
+ job.dispatch
65
+ @running_jobs << job
66
+
67
+ children_count -= 1
68
+ break unless children_count > 0
69
+ end
70
+
71
+ end
72
+ end
73
+
74
+ def stop
75
+ puts "Stopping #{@running_jobs.size} running employees..."
76
+
77
+ shutdown_running_jobs
78
+
79
+ puts "Job Boss stopped"
80
+ end
81
+
82
+ private
83
+ # Cleans up @running_jobs variable, getting rid of jobs which have
84
+ # completed, which have been cancelled, or which went MIA
85
+ def cleanup_running_jobs
86
+ Job.uncached do
87
+ @running_jobs = Job.running.where('id in (?)', @running_jobs)
88
+
89
+ cancelled_jobs = @running_jobs.select(&:cancelled?)
90
+ cancelled_jobs.each {|job| kill_job(job) }
91
+ @running_jobs -= cancelled_jobs
92
+
93
+ # Clean out any jobs whos processes have stopped running for some reason
94
+ @running_jobs = @running_jobs.select do |job|
95
+ begin
96
+ Process.kill(0, job.employee_pid.to_i)
97
+ rescue Errno::ESRCH
98
+ nil
99
+ end
100
+ end
101
+ end
102
+ end
103
+
104
+ # Total number of employees which can be run
105
+ def available_employees
106
+ cleanup_running_jobs
107
+
108
+ @@config.employee_limit - @running_jobs.size
109
+ end
110
+
111
+ def establish_active_record_connection
112
+ @@config.database_yaml_path = File.join(@@config.working_dir, @@config.database_yaml_path) unless @@config.database_yaml_path[0] == ?/
113
+
114
+ raise "Database YAML file missing (#{@@config.database_yaml_path})" unless File.exist?(@@config.database_yaml_path)
115
+
116
+ config = YAML.load(File.read(@@config.database_yaml_path))
117
+
118
+ ActiveRecord::Base.establish_connection(config[@@config.environment])
119
+ end
120
+
121
+ def require_job_classes
122
+ @@config.jobs_path = File.join(@@config.working_dir, @@config.jobs_path) unless @@config.jobs_path[0] == ?/
123
+
124
+ raise "Jobs path missing (#{@@config.jobs_path})" unless File.exist?(@@config.jobs_path)
125
+
126
+ Dir.glob(File.join(@@config.jobs_path, '*.rb')).each {|job_class| require job_class }
127
+ end
128
+
129
+ def migrate
130
+ unless Job.table_exists?
131
+ require 'migrate'
132
+ CreateJobs.up
133
+ end
134
+ end
135
+
136
+ def kill_job(job)
137
+ begin
138
+ Process.kill("HUP", job.employee_pid.to_i)
139
+ rescue Errno::ESRCH
140
+ nil
141
+ end
142
+ end
143
+
144
+ def shutdown_running_jobs
145
+ cleanup_running_jobs
146
+
147
+ @running_jobs.each do |job|
148
+ kill_job(job)
149
+ job.mark_for_redo
150
+ end
151
+ end
152
+ end
153
+ end
@@ -0,0 +1,8 @@
1
+ # Capistrano task for job_boss.
2
+ #
3
+ # Just add "require 'job_boss/capistrano'" in your Capistrano deploy.rb, and
4
+ # job_boss will be activated after each new deployment.
5
+
6
+ Capistrano::Configuration.instance(:must_exist).load do
7
+ after "deploy:update_code", "job_boss:restart"
8
+ end
@@ -0,0 +1,40 @@
1
+ module JobBoss
2
+ class Config
3
+ attr_accessor :working_dir, :database_yaml_path, :jobs_path, :sleep_interval, :employee_limit, :environment
4
+
5
+ def parse_args(argv, options = {})
6
+ @working_dir = options[:working_dir] || Dir.pwd
7
+ @database_yaml_path = 'config/database.yml'
8
+ @jobs_path = 'app/jobs'
9
+ @sleep_interval = 0.5
10
+ @employee_limit = 4
11
+ @environment = 'development'
12
+
13
+ require 'optparse'
14
+
15
+ OptionParser.new do |opts|
16
+ opts.banner = "Usage: job_boss [start|stop|restart|status|watch] [-- <options>]"
17
+
18
+ opts.on("-d", "--database-yaml PATH", "Path for database YAML (defaults to ./config/database.yml)") do |path|
19
+ @database_yaml_path = path
20
+ end
21
+
22
+ opts.on("-j", "--jobs-path PATH", "Path to folder with job classes (defaults to ./app/jobs)") do |path|
23
+ @database_yaml_path = path
24
+ end
25
+
26
+ opts.on("-e", "--environment ENV", "Rails environment to use in database YAML file (defaults to 'development')") do |env|
27
+ @environment = env
28
+ end
29
+
30
+ opts.on("-s", "--sleep-interval INTERVAL", Integer, "Number of seconds for the boss to sleep between checks of the queue (default 0.5)") do |interval|
31
+ @sleep_interval = interval
32
+ end
33
+
34
+ opts.on("-c", "--child-limit LIMIT", Integer, "Maximum number of employees (default 4)") do |limit|
35
+ @employee_limit = limit
36
+ end
37
+ end.parse!(argv)
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,155 @@
1
+
2
+ module JobBoss
3
+ class Job < ActiveRecord::Base
4
+ default_scope order('created_at')
5
+
6
+ serialize :args
7
+ serialize :result
8
+ serialize :error_backtrace
9
+
10
+ scope :pending, where('started_at IS NULL')
11
+ scope :running, where('started_at IS NOT NULL AND completed_at IS NULL')
12
+ scope :completed, where('completed_at IS NOT NULL')
13
+
14
+ # Method used by the boss to dispatch an employee
15
+ def dispatch
16
+ mark_as_started
17
+ puts "Dispatching Job ##{self.id}"
18
+
19
+ pid = fork do
20
+ $0 = "job_boss - employee (job ##{self.id})"
21
+ Process.setpriority(Process::PRIO_PROCESS, 0, 19)
22
+
23
+ begin
24
+ mark_employee
25
+
26
+ Signal.trap("HUP") do
27
+ mark_for_redo
28
+ end
29
+
30
+ result = self.class.call_path(self.path, *self.args)
31
+ self.update_attribute(:result, result)
32
+ rescue Exception => exception
33
+ mark_exception(exception)
34
+ puts "Error running job ##{self.id}!"
35
+ ensure
36
+ until mark_as_completed
37
+ sleep(1)
38
+ end
39
+
40
+ puts "Job ##{self.id} completed, exiting..."
41
+ Kernel.exit
42
+ end
43
+ end
44
+
45
+ Process.detach(pid)
46
+ end
47
+
48
+ # Clear out the job and put it back onto the queue for processing
49
+ def mark_for_redo
50
+ self.reload
51
+ self.started_at = nil
52
+ self.result = nil
53
+ self.completed_at = nil
54
+ self.status = nil
55
+ self.error_message = nil
56
+ self.error_backtrace = nil
57
+ self.employee_host = nil
58
+ self.employee_pid = nil
59
+ self.save
60
+ end
61
+
62
+ # Is the job complete?
63
+ def completed?
64
+ !!completed_at
65
+ end
66
+
67
+ # Mark the job as cancelled so that the boss won't run the job and so that
68
+ # the employee running the job gets stopped (if it's been dispatched)
69
+ def cancel
70
+ mark_as_cancelled
71
+ end
72
+
73
+ # Has the job been cancelled?
74
+ def cancelled?
75
+ !!cancelled_at
76
+ end
77
+
78
+ # Did the job succeed?
79
+ def succeeded?
80
+ completed_at && (status == 'success')
81
+ end
82
+
83
+ # How long did the job take?
84
+ def time_taken
85
+ completed_at - started_at if completed_at && started_at
86
+ end
87
+
88
+ class << self
89
+ def wait_for_jobs(jobs, sleep_interval = 0.5)
90
+ running_jobs = jobs.dup
91
+
92
+ until Job.running.find_all_by_id(running_jobs.collect(&:id)).empty?
93
+ sleep(sleep_interval)
94
+ end
95
+
96
+ true
97
+ end
98
+
99
+ def result_hash(jobs)
100
+ jobs.inject({}) do |hash, job|
101
+ hash.merge(job.args => job.result)
102
+ end
103
+ end
104
+ end
105
+
106
+ private
107
+
108
+ def mark_as_started
109
+ update_attributes(:started_at => Time.now)
110
+ end
111
+
112
+ def mark_as_cancelled
113
+ update_attributes(:cancelled_at => Time.now)
114
+ end
115
+
116
+ def mark_employee
117
+ require 'socket'
118
+ update_attributes(:employee_host => Socket.gethostname,
119
+ :employee_pid => Process.pid)
120
+ end
121
+
122
+ def mark_exception(exception)
123
+ update_attributes(:status => 'error', :error_message => exception.message, :error_backtrace => exception.backtrace)
124
+ end
125
+
126
+ def mark_as_completed
127
+ self.status ||= 'success'
128
+ self.completed_at = Time.now
129
+ self.save
130
+ end
131
+
132
+ class << self
133
+ def call_path(path, *args)
134
+ require 'active_support'
135
+
136
+ raise ArgumentError, "Invalid path (must have #)" unless path.match(/.#./)
137
+ controller, action = path.split('#')
138
+
139
+ controller_object = begin
140
+ Kernel.const_get("#{controller.classify}Jobs").new
141
+ rescue NameError
142
+ raise ArgumentError, "Invalid controller"
143
+ end
144
+
145
+ raise ArgumentError, "Invalid path action" unless controller_object.respond_to?(action)
146
+
147
+ controller_object.send(action, *args)
148
+ end
149
+
150
+ def pending_paths
151
+ self.pending.except(:order).select('DISTINCT path').collect(&:path)
152
+ end
153
+ end
154
+ end
155
+ end
@@ -0,0 +1,37 @@
1
+ module JobBoss
2
+ class Queuer
3
+ def method_missing(method_id, *args)
4
+ require 'active_support'
5
+
6
+ method_name = method_id.id2name
7
+
8
+ if @class && @controller
9
+ # In here, we've already figured out the class, so assume the method_missing call is to the method
10
+
11
+ if @class.respond_to?(method_name)
12
+ require 'job_boss/job'
13
+ path = "#{@controller}##{method_name}"
14
+
15
+ @class = nil
16
+ @controller = nil
17
+
18
+ Job.create(:path => path,
19
+ :args => args)
20
+ else
21
+ raise ArgumentError, "Invalid action"
22
+ end
23
+ else
24
+ # Check to see if there's a jobs class
25
+ begin
26
+ @class = Kernel.const_get("#{method_name.classify}Jobs").new
27
+
28
+ @controller = method_name
29
+ rescue NameError
30
+ raise ArgumentError, "Invalid controller"
31
+ end
32
+
33
+ self
34
+ end
35
+ end
36
+ end
37
+ end
data/lib/migrate.rb ADDED
@@ -0,0 +1,27 @@
1
+ class CreateJobs < ActiveRecord::Migration
2
+ def self.up
3
+ create_table :jobs do |t|
4
+ t.string :path
5
+ t.text :args
6
+ t.text :result
7
+ t.datetime :started_at
8
+ t.datetime :cancelled_at
9
+ t.datetime :completed_at
10
+ t.string :status
11
+ t.string :error_message
12
+ t.text :error_backtrace
13
+
14
+ t.string :employee_host
15
+ t.string :employee_pid
16
+
17
+ t.timestamps
18
+ end
19
+
20
+ add_index :jobs, :path
21
+ add_index :jobs, :status
22
+ end
23
+
24
+ def self.down
25
+ drop_table :jobs
26
+ end
27
+ end
metadata ADDED
@@ -0,0 +1,151 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: job_boss
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 0
7
+ - 2
8
+ version: "0.2"
9
+ platform: ruby
10
+ authors:
11
+ - Brian Underwood
12
+ autorequire:
13
+ bindir: bin
14
+ cert_chain: []
15
+
16
+ date: 2010-11-26 00:00:00 -05:00
17
+ default_executable: job_boss
18
+ dependencies:
19
+ - !ruby/object:Gem::Dependency
20
+ name: activerecord
21
+ prerelease: false
22
+ requirement: &id001 !ruby/object:Gem::Requirement
23
+ none: false
24
+ requirements:
25
+ - - ">="
26
+ - !ruby/object:Gem::Version
27
+ segments:
28
+ - 0
29
+ version: "0"
30
+ type: :runtime
31
+ version_requirements: *id001
32
+ - !ruby/object:Gem::Dependency
33
+ name: activesupport
34
+ prerelease: false
35
+ requirement: &id002 !ruby/object:Gem::Requirement
36
+ none: false
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ segments:
41
+ - 0
42
+ version: "0"
43
+ type: :runtime
44
+ version_requirements: *id002
45
+ - !ruby/object:Gem::Dependency
46
+ name: daemons
47
+ prerelease: false
48
+ requirement: &id003 !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ segments:
54
+ - 0
55
+ version: "0"
56
+ type: :runtime
57
+ version_requirements: *id003
58
+ description: job_boss allows you to queue jobs which are unqueued by a "Job Boss" daemon and handed off to workers to process
59
+ email:
60
+ - ml+job_boss@semi-sentient.com
61
+ executables:
62
+ - job_boss
63
+ extensions: []
64
+
65
+ extra_rdoc_files: []
66
+
67
+ files:
68
+ - README.markdown
69
+ - Rakefile
70
+ - bin/job_boss
71
+ - doc/ActiveRecord.html
72
+ - doc/ActiveRecord/Base.html
73
+ - doc/CreateJobs.html
74
+ - doc/JobBoss.html
75
+ - doc/JobBoss/Boss.html
76
+ - doc/JobBoss/Config.html
77
+ - doc/JobBoss/Job.html
78
+ - doc/JobBoss/Queuer.html
79
+ - doc/Mongrel.html
80
+ - doc/Mongrel/HttpServer.html
81
+ - doc/Passenger.html
82
+ - doc/Passenger/Railz.html
83
+ - doc/Passenger/Railz/RequestHandler.html
84
+ - doc/PhusionPassenger.html
85
+ - doc/PhusionPassenger/Rack.html
86
+ - doc/PhusionPassenger/Rack/RequestHandler.html
87
+ - doc/PhusionPassenger/Railz.html
88
+ - doc/PhusionPassenger/Railz/RequestHandler.html
89
+ - doc/Rakefile.html
90
+ - doc/Spawn.html
91
+ - doc/Spawn/SpawnId.html
92
+ - doc/bin/job_boss.html
93
+ - doc/created.rid
94
+ - doc/index.html
95
+ - doc/lib/job_boss/boss_rb.html
96
+ - doc/lib/job_boss/capistrano_rb.html
97
+ - doc/lib/job_boss/configuror_rb.html
98
+ - doc/lib/job_boss/job_rb.html
99
+ - doc/lib/job_boss/queuer_rb.html
100
+ - doc/lib/migrate_rb.html
101
+ - doc/rdoc.css
102
+ - doc/vendor/spawn/CHANGELOG.html
103
+ - doc/vendor/spawn/LICENSE.html
104
+ - doc/vendor/spawn/init_rb.html
105
+ - doc/vendor/spawn/lib/patches_rb.html
106
+ - doc/vendor/spawn/lib/spawn_rb.html
107
+ - job_boss.gemspec
108
+ - lib/job_boss/boss.rb
109
+ - lib/job_boss/capistrano.rb
110
+ - lib/job_boss/configuror.rb
111
+ - lib/job_boss/job.rb
112
+ - lib/job_boss/queuer.rb
113
+ - lib/migrate.rb
114
+ has_rdoc: true
115
+ homepage: http://github.com/cheerfulstoic/job_boss
116
+ licenses: []
117
+
118
+ post_install_message:
119
+ rdoc_options: []
120
+
121
+ require_paths:
122
+ - lib
123
+ - vendor
124
+ - vendor/spawn/lib
125
+ required_ruby_version: !ruby/object:Gem::Requirement
126
+ none: false
127
+ requirements:
128
+ - - ">="
129
+ - !ruby/object:Gem::Version
130
+ segments:
131
+ - 0
132
+ version: "0"
133
+ required_rubygems_version: !ruby/object:Gem::Requirement
134
+ none: false
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ segments:
139
+ - 1
140
+ - 3
141
+ - 6
142
+ version: 1.3.6
143
+ requirements: []
144
+
145
+ rubyforge_project:
146
+ rubygems_version: 1.3.7
147
+ signing_key:
148
+ specification_version: 3
149
+ summary: Asyncronous, parallel job processing
150
+ test_files: []
151
+