require 'user' class BackgroundJob < ActiveRecord::Base require 'remix_methods' serialize :arguments TYPE_LOCAL = 'local' STATUS_NEW = 'new' STATUS_PROGRESS = 'progress' STATUS_COMPLETE = 'complete' STATUS_FAILED = 'failed' # Runs the job def run self.update_attributes(:started_at => Time.now, :status => STATUS_PROGRESS) log "Started Job: #{self.id} (#{self.class_name}.#{self.method_name}) at #{self.started_at}" # Start the job begin self.class_name.constantize.send(self.method_name, *self.arguments) do |message| self.update_attributes(:message => message) end self.update_attributes(:finished_at => Time.now, :status => STATUS_COMPLETE) rescue Exception => err log "Error from background job #{self.id}: #{err.message}" log err.backtrace.join("\n") msg = err.message + " (" + err.backtrace[0] + ")" self.update_attributes(:finished_at => Time.now, :status => STATUS_FAILED, :message => msg[0,198]) end log "Finished Job: #{self.id} at #{self.finished_at}" end def self.start RemixMethods.register_pid('background_jobs') ActiveRecord::Base.logger = Logger.new(RAILS_ROOT + '/log/jobs.log') ActiveRecord::Base.logger.level = Logger::DEBUG while true jobs = BackgroundJob.find(:all, :conditions => "job_type IS NULL and (started_at IS NULL OR finished_at IS NULL) AND created_at > now() - INTERVAL '3 day' \ AND (start_at IS NULL OR start_at < now())", :order => 'created_at') jobs.each do |job| job.run end sleep 10 end end def self.create_job(class_name, method, *args) BackgroundJob.create(:class_name => class_name.to_s, :method_name => method.to_s, :arguments => args) end def self.create_script_job(script, options = {}) BackgroundJob.create(:class_name => 'Kernel', :method_name => 'eval', :arguments => script, :start_at => options[:start_at]) end def self.create_local_job(requester, class_name, method, *args) BackgroundJob.create(:class_name => class_name.to_s, :method_name => method.to_s, :arguments => args, :job_type => TYPE_LOCAL, :status => STATUS_NEW) end def self.test_message(&callback) if callback callback.call("step 1") end puts "background job, this is the test_message method, sleeping for 20 secs" if callback callback.call("step 2") end sleep 20 puts "After sleep, last message" if callback callback.call("done") end end def self.test2 puts "I expected no block" end # Spawn a new background process to execute this background job immediately. We have # to muck with re-creating our ActiveRecord connections because AR doesn't normally survive fork. # I wonder what else craps out... def spawn # Don't even bother on windows return if RUBY_PLATFORM.index("mswin") self.reload if self.status == nil || self.status == STATUS_NEW dbconfig = ActiveRecord::Base.remove_connection pid = fork do begin # Monkey-path Mongrel to not remove its pid file in the child require 'mongrel' Mongrel::Configurator.class_eval("def remove_pid_file; puts 'child no-op'; end") ActiveRecord::Base.establish_connection(dbconfig) run ensure ActiveRecord::Base.remove_connection end end Process.detach(pid) ActiveRecord::Base.establish_connection(dbconfig) end end end