delayed_job 2.1.0.pre2 → 2.1.1

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.
@@ -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