delayed_job 2.1.0.pre2 → 2.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -44,8 +44,9 @@ module Delayed
44
44
  opts.on('-m', '--monitor', 'Start monitor process.') do
45
45
  @monitor = true
46
46
  end
47
-
48
-
47
+ opts.on('--sleep-delay N', "Amount of time to sleep when no jobs are found") do |n|
48
+ @options[:sleep_delay] = n
49
+ end
49
50
  end
50
51
  @args = opts.parse!(args)
51
52
  end
@@ -0,0 +1,4 @@
1
+ module Delayed
2
+ class DeserializationError < StandardError
3
+ end
4
+ end
@@ -3,25 +3,23 @@ require 'active_support/core_ext/module/aliasing'
3
3
 
4
4
  module Delayed
5
5
  class DelayProxy < ActiveSupport::BasicObject
6
- def initialize(target, options)
6
+ def initialize(payload_class, target, options)
7
+ @payload_class = payload_class
7
8
  @target = target
8
9
  @options = options
9
10
  end
10
11
 
11
12
  def method_missing(method, *args)
12
- Job.create({
13
- :payload_object => PerformableMethod.new(@target, method.to_sym, args),
14
- :priority => ::Delayed::Worker.default_priority
15
- }.merge(@options))
13
+ Job.enqueue({:payload_object => @payload_class.new(@target, method.to_sym, args)}.merge(@options))
16
14
  end
17
15
  end
18
16
 
19
17
  module MessageSending
20
18
  def delay(options = {})
21
- DelayProxy.new(self, options)
19
+ DelayProxy.new(PerformableMethod, self, options)
22
20
  end
23
21
  alias __delay__ delay
24
-
22
+
25
23
  def send_later(method, *args)
26
24
  warn "[DEPRECATION] `object.send_later(:method)` is deprecated. Use `object.delay.method"
27
25
  __delay__.__send__(method, *args)
@@ -31,16 +29,26 @@ module Delayed
31
29
  warn "[DEPRECATION] `object.send_at(time, :method)` is deprecated. Use `object.delay(:run_at => time).method"
32
30
  __delay__(:run_at => time).__send__(method, *args)
33
31
  end
34
-
32
+
35
33
  module ClassMethods
36
- def handle_asynchronously(method)
34
+ def handle_asynchronously(method, opts = {})
37
35
  aliased_method, punctuation = method.to_s.sub(/([?!=])$/, ''), $1
38
36
  with_method, without_method = "#{aliased_method}_with_delay#{punctuation}", "#{aliased_method}_without_delay#{punctuation}"
39
37
  define_method(with_method) do |*args|
40
- delay.__send__(without_method, *args)
38
+ curr_opts = opts.clone
39
+ curr_opts.each_key do |key|
40
+ if (val = curr_opts[key]).is_a?(Proc)
41
+ curr_opts[key] = if val.arity == 1
42
+ val.call(self)
43
+ else
44
+ val.call
45
+ end
46
+ end
47
+ end
48
+ delay(curr_opts).__send__(without_method, *args)
41
49
  end
42
50
  alias_method_chain method, :delay
43
51
  end
44
52
  end
45
- end
53
+ end
46
54
  end
@@ -0,0 +1,21 @@
1
+ require 'action_mailer'
2
+
3
+ module Delayed
4
+ class PerformableMailer < PerformableMethod
5
+ def perform
6
+ object.send(method_name, *args).deliver
7
+ end
8
+ end
9
+ end
10
+
11
+ ActionMailer::Base.class_eval do
12
+ def self.delay(options = {})
13
+ Delayed::DelayProxy.new(Delayed::PerformableMailer, self, options)
14
+ end
15
+ end
16
+
17
+ Mail::Message.class_eval do
18
+ def delay(*args)
19
+ raise RuntimeError, "Use MyMailer.delay.mailer_action(args) to delay sending of emails."
20
+ end
21
+ end
@@ -1,27 +1,31 @@
1
+ require 'active_support/core_ext/module/delegation'
2
+
1
3
  module Delayed
2
- class PerformableMethod < Struct.new(:object, :method, :args)
3
- def initialize(object, method, args)
4
- raise NoMethodError, "undefined method `#{method}' for #{object.inspect}" unless object.respond_to?(method, true)
4
+ class PerformableMethod < Struct.new(:object, :method_name, :args)
5
+ delegate :method, :to => :object
6
+
7
+ def initialize(object, method_name, args)
8
+ raise NoMethodError, "undefined method `#{method_name}' for #{object.inspect}" unless object.respond_to?(method_name, true)
5
9
 
6
- self.object = object
7
- self.args = args
8
- self.method = method.to_sym
10
+ self.object = object
11
+ self.args = args
12
+ self.method_name = method_name.to_sym
9
13
  end
10
-
14
+
11
15
  def display_name
12
- "#{object.class}##{method}"
16
+ "#{object.class}##{method_name}"
13
17
  end
14
-
18
+
15
19
  def perform
16
- object.send(method, *args) if object
20
+ object.send(method_name, *args) if object
17
21
  end
18
-
22
+
19
23
  def method_missing(symbol, *args)
20
- object.respond_to?(symbol) ? object.send(symbol, *args) : super
24
+ object.send(symbol, *args)
21
25
  end
22
-
26
+
23
27
  def respond_to?(symbol, include_private=false)
24
- object.respond_to?(symbol, include_private) || super
25
- end
28
+ super || object.respond_to?(symbol, include_private)
29
+ end
26
30
  end
27
31
  end
@@ -13,6 +13,10 @@ Capistrano::Configuration.instance.load do
13
13
  fetch(:rails_env, false) ? "RAILS_ENV=#{fetch(:rails_env)}" : ''
14
14
  end
15
15
 
16
+ def args
17
+ fetch(:delayed_job_args, "")
18
+ end
19
+
16
20
  desc "Stop the delayed_job process"
17
21
  task :stop, :roles => :app do
18
22
  run "cd #{current_path};#{rails_env} script/delayed_job stop"
@@ -20,12 +24,12 @@ Capistrano::Configuration.instance.load do
20
24
 
21
25
  desc "Start the delayed_job process"
22
26
  task :start, :roles => :app do
23
- run "cd #{current_path};#{rails_env} script/delayed_job start"
27
+ run "cd #{current_path};#{rails_env} script/delayed_job start #{args}"
24
28
  end
25
29
 
26
30
  desc "Restart the delayed_job process"
27
31
  task :restart, :roles => :app do
28
- run "cd #{current_path};#{rails_env} script/delayed_job restart"
32
+ run "cd #{current_path};#{rails_env} script/delayed_job restart #{args}"
29
33
  end
30
34
  end
31
35
  end
@@ -0,0 +1,13 @@
1
+ class ActiveRecord::Base
2
+ yaml_as "tag:ruby.yaml.org,2002:ActiveRecord"
3
+
4
+ def self.yaml_new(klass, tag, val)
5
+ klass.find(val['attributes']['id'])
6
+ rescue ActiveRecord::RecordNotFound
7
+ raise Delayed::DeserializationError
8
+ end
9
+
10
+ def to_yaml_properties
11
+ ['@attributes']
12
+ end
13
+ end
@@ -11,12 +11,12 @@ module Delayed
11
11
  self.max_attempts = 25
12
12
  self.max_run_time = 4.hours
13
13
  self.default_priority = 0
14
-
14
+
15
15
  # By default failed jobs are destroyed after too many attempts. If you want to keep them around
16
16
  # (perhaps to inspect the reason for the failure), set this to false.
17
17
  cattr_accessor :destroy_failed_jobs
18
18
  self.destroy_failed_jobs = true
19
-
19
+
20
20
  self.logger = if defined?(Rails)
21
21
  Rails.logger
22
22
  elsif defined?(RAILS_DEFAULT_LOGGER)
@@ -25,26 +25,28 @@ module Delayed
25
25
 
26
26
  # name_prefix is ignored if name is set directly
27
27
  attr_accessor :name_prefix
28
-
28
+
29
29
  cattr_reader :backend
30
-
30
+
31
31
  def self.backend=(backend)
32
32
  if backend.is_a? Symbol
33
+ require "delayed/serialization/#{backend}"
33
34
  require "delayed/backend/#{backend}"
34
35
  backend = "Delayed::Backend::#{backend.to_s.classify}::Job".constantize
35
36
  end
36
37
  @@backend = backend
37
38
  silence_warnings { ::Delayed.const_set(:Job, backend) }
38
39
  end
39
-
40
+
40
41
  def self.guess_backend
41
42
  self.backend ||= :active_record if defined?(ActiveRecord)
42
43
  end
43
44
 
44
45
  def initialize(options={})
45
- @quiet = options[:quiet]
46
+ @quiet = options.has_key?(:quiet) ? options[:quiet] : true
46
47
  self.class.min_priority = options[:min_priority] if options.has_key?(:min_priority)
47
48
  self.class.max_priority = options[:max_priority] if options.has_key?(:max_priority)
49
+ self.class.sleep_delay = options[:sleep_delay] if options.has_key?(:sleep_delay)
48
50
  end
49
51
 
50
52
  # Every worker has a unique name which by default is the pid of the process. There are some
@@ -80,7 +82,7 @@ module Delayed
80
82
  break if $exit
81
83
 
82
84
  if count.zero?
83
- sleep(@@sleep_delay)
85
+ sleep(self.class.sleep_delay)
84
86
  else
85
87
  say "#{count} jobs processed at %.4f j/s, %d failed ..." % [count / realtime, result.last]
86
88
  end
@@ -91,7 +93,7 @@ module Delayed
91
93
  ensure
92
94
  Delayed::Job.clear_locks!(name)
93
95
  end
94
-
96
+
95
97
  # Do num jobs and return stats on success/failure.
96
98
  # Exit early if interrupted.
97
99
  def work_off(num = 100)
@@ -111,7 +113,7 @@ module Delayed
111
113
 
112
114
  return [success, failure]
113
115
  end
114
-
116
+
115
117
  def run(job)
116
118
  runtime = Benchmark.realtime do
117
119
  Timeout.timeout(self.class.max_run_time.to_i) { job.invoke_job }
@@ -119,34 +121,34 @@ module Delayed
119
121
  end
120
122
  say "#{job.name} completed after %.4f" % runtime
121
123
  return true # did work
122
- rescue Exception => e
123
- handle_failed_job(job, e)
124
+ rescue DeserializationError => error
125
+ job.last_error = "{#{error.message}\n#{error.backtrace.join('\n')}"
126
+ failed(job)
127
+ rescue Exception => error
128
+ handle_failed_job(job, error)
124
129
  return false # work failed
125
130
  end
126
-
131
+
127
132
  # Reschedule the job in the future (when a job fails).
128
133
  # Uses an exponential scale depending on the number of failed attempts.
129
134
  def reschedule(job, time = nil)
130
135
  if (job.attempts += 1) < self.class.max_attempts
131
- time ||= Job.db_time_now + (job.attempts ** 4) + 5
136
+ time ||= job.reschedule_at
132
137
  job.run_at = time
133
138
  job.unlock
134
139
  job.save!
135
140
  else
136
141
  say "PERMANENTLY removing #{job.name} because of #{job.attempts} consecutive failures.", Logger::INFO
142
+ failed(job)
143
+ end
144
+ end
137
145
 
138
- if job.payload_object.respond_to? :on_permanent_failure
139
- say "Running on_permanent_failure hook"
140
- failure_method = job.payload_object.method(:on_permanent_failure)
141
- if failure_method.arity == 1
142
- failure_method.call(job)
143
- else
144
- failure_method.call
145
- end
146
- end
147
-
148
- self.class.destroy_failed_jobs ? job.destroy : job.update_attributes(:failed_at => Delayed::Job.db_time_now)
146
+ def failed(job)
147
+ job.hook(:failure)
148
+ if job.respond_to?(:on_permanent_failure)
149
+ warn "[DEPRECATION] The #on_permanent_failure hook has been renamed to #failure."
149
150
  end
151
+ self.class.destroy_failed_jobs ? job.destroy : job.update_attributes(:failed_at => Delayed::Job.db_time_now)
150
152
  end
151
153
 
152
154
  def say(text, level = Logger::INFO)
@@ -156,30 +158,19 @@ module Delayed
156
158
  end
157
159
 
158
160
  protected
159
-
161
+
160
162
  def handle_failed_job(job, error)
161
- job.last_error = error.message + "\n" + error.backtrace.join("\n")
163
+ job.last_error = "{#{error.message}\n#{error.backtrace.join('\n')}"
162
164
  say "#{job.name} failed with #{error.class.name}: #{error.message} - #{job.attempts} failed attempts", Logger::ERROR
163
165
  reschedule(job)
164
166
  end
165
-
167
+
166
168
  # Run the next job we can get an exclusive lock on.
167
169
  # If no jobs are left we return nil
168
170
  def reserve_and_run_one_job
169
-
170
- # We get up to 5 jobs from the db. In case we cannot get exclusive access to a job we try the next.
171
- # this leads to a more even distribution of jobs across the worker processes
172
- job = Delayed::Job.find_available(name, 5, self.class.max_run_time).detect do |job|
173
- if job.lock_exclusively!(self.class.max_run_time, name)
174
- say "acquired lock on #{job.name}"
175
- true
176
- else
177
- say "failed to acquire exclusive lock for #{job.name}", Logger::WARN
178
- false
179
- end
180
- end
181
-
171
+ job = Delayed::Job.reserve(self)
182
172
  run(job) if job
183
173
  end
184
174
  end
175
+
185
176
  end
data/lib/delayed_job.rb CHANGED
@@ -2,10 +2,12 @@ require 'active_support'
2
2
 
3
3
  require File.dirname(__FILE__) + '/delayed/message_sending'
4
4
  require File.dirname(__FILE__) + '/delayed/performable_method'
5
+ require File.dirname(__FILE__) + '/delayed/performable_mailer' if defined?(ActionMailer)
5
6
  require File.dirname(__FILE__) + '/delayed/yaml_ext'
6
7
  require File.dirname(__FILE__) + '/delayed/backend/base'
7
8
  require File.dirname(__FILE__) + '/delayed/worker'
8
- require File.dirname(__FILE__) + '/delayed/railtie' if defined?(::Rails::Railtie)
9
+ require File.dirname(__FILE__) + '/delayed/deserialization_error'
10
+ require File.dirname(__FILE__) + '/delayed/railtie' if defined?(Rails::Railtie)
9
11
 
10
- Object.send(:include, Delayed::MessageSending)
12
+ Object.send(:include, Delayed::MessageSending)
11
13
  Module.send(:include, Delayed::MessageSending::ClassMethods)
@@ -5,7 +5,7 @@ describe Delayed::Backend::ActiveRecord::Job do
5
5
  after do
6
6
  Time.zone = nil
7
7
  end
8
-
8
+
9
9
  it_should_behave_like 'a delayed_job backend'
10
10
 
11
11
  context "db_time_now" do
@@ -13,7 +13,7 @@ describe Delayed::Backend::ActiveRecord::Job do
13
13
  Time.zone = 'Eastern Time (US & Canada)'
14
14
  %w(EST EDT).should include(Delayed::Job.db_time_now.zone)
15
15
  end
16
-
16
+
17
17
  it "should return UTC time if that is the AR default" do
18
18
  Time.zone = nil
19
19
  ActiveRecord::Base.default_timezone = :utc
@@ -26,10 +26,10 @@ describe Delayed::Backend::ActiveRecord::Job do
26
26
  %w(CST CDT).should include(Delayed::Backend::ActiveRecord::Job.db_time_now.zone)
27
27
  end
28
28
  end
29
-
29
+
30
30
  describe "after_fork" do
31
31
  it "should call reconnect on the connection" do
32
- ActiveRecord::Base.connection.should_receive(:reconnect!)
32
+ ActiveRecord::Base.should_receive(:establish_connection)
33
33
  Delayed::Backend::ActiveRecord::Job.after_fork
34
34
  end
35
35
  end
data/spec/database.yml ADDED
@@ -0,0 +1,4 @@
1
+ mysql:
2
+ adapter: mysql
3
+ database: delayed_job
4
+ username: root
@@ -7,21 +7,59 @@ describe Delayed::MessageSending do
7
7
  end
8
8
  handle_asynchronously :tell!
9
9
  end
10
-
10
+
11
11
  it "should alias original method" do
12
12
  Story.new.should respond_to(:tell_without_delay!)
13
13
  Story.new.should respond_to(:tell_with_delay!)
14
14
  end
15
-
15
+
16
16
  it "should create a PerformableMethod" do
17
17
  story = Story.create!
18
18
  lambda {
19
19
  job = story.tell!(1)
20
20
  job.payload_object.class.should == Delayed::PerformableMethod
21
- job.payload_object.method.should == :tell_without_delay!
21
+ job.payload_object.method_name.should == :tell_without_delay!
22
22
  job.payload_object.args.should == [1]
23
23
  }.should change { Delayed::Job.count }
24
24
  end
25
+
26
+ describe 'with options' do
27
+ class Fable
28
+ class << self
29
+ attr_accessor :importance
30
+ end
31
+ def tell
32
+ end
33
+ handle_asynchronously :tell, :priority => Proc.new { self.importance }
34
+ end
35
+
36
+ it 'should set the priority based on the Fable importance' do
37
+ Fable.importance = 10
38
+ job = Fable.new.tell
39
+ job.priority.should == 10
40
+
41
+ Fable.importance = 20
42
+ job = Fable.new.tell
43
+ job.priority.should == 20
44
+ end
45
+
46
+ describe 'using a proc with parament' do
47
+ class Yarn
48
+ attr_accessor :importance
49
+ def spin
50
+ end
51
+ handle_asynchronously :spin, :priority => Proc.new {|y| y.importance }
52
+ end
53
+
54
+ it 'should set the priority based on the Fable importance' do
55
+ job = Yarn.new.tap {|y| y.importance = 10 }.spin
56
+ job.priority.should == 10
57
+
58
+ job = Yarn.new.tap {|y| y.importance = 20 }.spin
59
+ job.priority.should == 20
60
+ end
61
+ end
62
+ end
25
63
  end
26
64
 
27
65
  context "delay" do
@@ -29,7 +67,7 @@ describe Delayed::MessageSending do
29
67
  lambda {
30
68
  job = "hello".delay.count('l')
31
69
  job.payload_object.class.should == Delayed::PerformableMethod
32
- job.payload_object.method.should == :count
70
+ job.payload_object.method_name.should == :count
33
71
  job.payload_object.args.should == ['l']
34
72
  }.should change { Delayed::Job.count }.by(1)
35
73
  end