public
Description: Database based asynchronously priority queue system -- Extracted from Shopify
Homepage: http://www.shopify.com
Clone URL: git://github.com/tobi/delayed_job.git
delayed_job / lib / delayed / job.rb
100644 145 lines (111 sloc) 4.487 kb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
module Delayed
 
  class DeserializationError < StandardError
  end
 
  class Job < ActiveRecord::Base
    ParseObjectFromYaml = /\!ruby\/\w+\:([^\s]+)/
    
    set_table_name :delayed_jobs
  
    class Runner
      attr_accessor :logger, :jobs
      attr_accessor :runs, :success, :failure
    
      def initialize(jobs, logger = nil)
        @jobs = jobs
        @logger = logger
        self.runs = self.success = self.failure = 0
      end
      
      def run
        
        ActiveRecord::Base.cache do
          ActiveRecord::Base.transaction do
            @jobs.each do |job|
              self.runs += 1
              begin
                time = Benchmark.measure do
                  job.perform
                  ActiveRecord::Base.uncached { job.destroy }
                  self.success += 1
                end
                logger.debug "Executed job in #{time.real}"
              rescue DeserializationError, StandardError, RuntimeError => e
                if logger
                  logger.error "Job #{job.id}: #{e.class} #{e.message}"
                  logger.error e.backtrace.join("\n")
                end
                ActiveRecord::Base.uncached { job.reshedule e.message }
                self.failure += 1
              end
            end
          end
        end
      
        self
      end
    end
   
    def self.enqueue(object, priority = 0)
      raise ArgumentError, 'Cannot enqueue items which do not respond to perform' unless object.respond_to?(:perform)
    
      Job.create(:handler => object, :priority => priority)
    end
  
    def handler=(object)
      self['handler'] = object.to_yaml
    end
  
    def handler
      @handler ||= deserialize(self['handler'])
    end
  
    def perform
      handler.perform
    end
  
    def reshedule(message)
      self.attempts += 1
      self.run_at = self.class.time_now + (attempts ** 4).seconds
      self.last_error = message
      save!
    end
  
    def self.peek(limit = 1)
      if limit == 1
        find(:first, :order => "priority DESC, run_at ASC", :conditions => ['run_at <= ?', time_now])
      else
        find(:all, :order => "priority DESC, run_at ASC", :limit => limit, :conditions => ['run_at <= ?', time_now])
      end
    end
    
    def self.work_off(limit = 100)
      jobs = Job.find(:all, :conditions => ['run_at <= ?', time_now], :order => "priority DESC, run_at ASC", :limit => limit)
    
      Job::Runner.new(jobs, logger).run
    end
  
    protected
    
    def self.time_now
      (ActiveRecord::Base.default_timezone == :utc) ? Time.now.utc : Time.now
    end
    
    def before_save
      self.run_at ||= self.class.time_now
    end
    
    private
    
    def deserialize(source)
      attempt_to_load_file = true
      
      begin
        handler = YAML.load(source) rescue nil
        return handler if handler.respond_to?(:perform)
        
        if handler.nil?
          if source =~ ParseObjectFromYaml
 
            # Constantize the object so that ActiveSupport can attempt
            # its auto loading magic. Will raise LoadError if not successful.
            attempt_to_load($1)
 
            # If successful, retry the yaml.load
            handler = YAML.load(source)
            return handler if handler.respond_to?(:perform)
          end
        end
        
        if handler.is_a?(YAML::Object)
          
          # Constantize the object so that ActiveSupport can attempt
          # its auto loading magic. Will raise LoadError if not successful.
          attempt_to_load(handler.class)
        
          # If successful, retry the yaml.load
          handler = YAML.load(source)
          return handler if handler.respond_to?(:perform)
        end
                  
        raise DeserializationError, 'Job failed to load: Unknown handler. Try to manually require the appropiate file.'
             
      rescue TypeError, LoadError, NameError => e
        
        raise DeserializationError, "Job failed to load: #{e.message}. Try to manually require the required file."
      end
    end
    
    def attempt_to_load(klass)
       klass.constantize
    end
  
  end
end