Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Merge remote branch 'collectiveidea/master' into delayed_job_daemon

  • Loading branch information...
commit 6f636600364a5b48047aeb5354177ca9b60793cc 2 parents 65e58c5 + 2bcbb50
guns authored
View
2  Gemfile
@@ -5,6 +5,6 @@ gem 'daemons'
group :development do
gem 'rspec'
gem 'rake'
- gem 'activerecord', '~>3'
+ gem 'rails', '~>3'
gem 'sqlite3-ruby'
end
View
48 Gemfile.lock
@@ -1,6 +1,20 @@
GEM
remote: http://rubygems.org/
specs:
+ abstract (1.0.0)
+ actionmailer (3.0.0)
+ actionpack (= 3.0.0)
+ mail (~> 2.2.5)
+ actionpack (3.0.0)
+ activemodel (= 3.0.0)
+ activesupport (= 3.0.0)
+ builder (~> 2.1.2)
+ erubis (~> 2.6.6)
+ i18n (~> 0.4.1)
+ rack (~> 1.2.1)
+ rack-mount (~> 0.6.12)
+ rack-test (~> 0.5.4)
+ tzinfo (~> 0.3.23)
activemodel (3.0.0)
activesupport (= 3.0.0)
builder (~> 2.1.2)
@@ -10,24 +24,56 @@ GEM
activesupport (= 3.0.0)
arel (~> 1.0.0)
tzinfo (~> 0.3.23)
+ activeresource (3.0.0)
+ activemodel (= 3.0.0)
+ activesupport (= 3.0.0)
activesupport (3.0.0)
arel (1.0.1)
activesupport (~> 3.0.0)
builder (2.1.2)
daemons (1.1.0)
+ erubis (2.6.6)
+ abstract (>= 1.0.0)
i18n (0.4.1)
+ mail (2.2.5)
+ activesupport (>= 2.3.6)
+ mime-types
+ treetop (>= 1.4.5)
+ mime-types (1.16)
+ polyglot (0.3.1)
+ rack (1.2.1)
+ rack-mount (0.6.13)
+ rack (>= 1.0.0)
+ rack-test (0.5.4)
+ rack (>= 1.0)
+ rails (3.0.0)
+ actionmailer (= 3.0.0)
+ actionpack (= 3.0.0)
+ activerecord (= 3.0.0)
+ activeresource (= 3.0.0)
+ activesupport (= 3.0.0)
+ bundler (~> 1.0.0)
+ railties (= 3.0.0)
+ railties (3.0.0)
+ actionpack (= 3.0.0)
+ activesupport (= 3.0.0)
+ rake (>= 0.8.4)
+ thor (~> 0.14.0)
rake (0.8.7)
rspec (1.3.0)
sqlite3-ruby (1.3.1)
+ thor (0.14.0)
+ treetop (1.4.8)
+ polyglot (>= 0.3.1)
tzinfo (0.3.23)
PLATFORMS
ruby
DEPENDENCIES
- activerecord (~> 3)
activesupport (~> 3)
daemons
+ rails (~> 3)
rake
rspec
sqlite3-ruby
View
26 README.textile
@@ -108,15 +108,33 @@ end
Delayed::Job.enqueue NewsletterJob.new('lorem ipsum...', Customers.find(:all).collect(&:email))
</pre>
-You can also add an optional on_permanent_failure method which will run if the job has failed too many times to be retried:
+h2. Hooks
+
+You can define hooks on your job that will be called at different stages in the process:
<pre>
class ParanoidNewsletterJob < NewsletterJob
def perform
emails.each { |e| NewsletterMailer.deliver_text_to_email(text, e) }
- end
+ end
+
+ def before(job)
+ record_stat 'newsletter_job/start'
+ end
+
+ def after(job)
+ record_stat 'newsletter_job/after'
+ end
+
+ def success(job)
+ record_stat 'newsletter_job/success'
+ end
- def on_permanent_failure
+ def error(job, exception)
+ notify_hoptoad(exception)
+ end
+
+ def failure
page_sysadmin_in_the_middle_of_the_night
end
end
@@ -151,6 +169,8 @@ make sure your job doesn't exceed this time. You should set this to the longest
By default, it will delete failed jobs (and it always deletes successful jobs). If you want to keep failed jobs, set
Delayed::Worker.destroy_failed_jobs = false. The failed jobs will be marked with non-null failed_at.
+By default all jobs are scheduled with priority = 0, which is top priority. You can change this by setting Delayed::Worker.default_priority to something else. Lower numbers have higher priority.
+
Here is an example of changing job parameters in Rails:
<pre>
View
4 delayed_job.gemspec
@@ -10,8 +10,8 @@ Gem::Specification.new do |s|
This gem is collectiveidea's fork (http://github.com/collectiveidea/delayed_job)."
s.email = 'brandon@collectiveidea.com'
s.extra_rdoc_files = 'README.textile'
- s.files = Dir.glob('{contrib,generators,lib,rails,recipes,spec,tasks}/**/*') +
- %w(init.rb MIT-LICENSE README.textile)
+ s.files = Dir.glob('{contrib,lib,recipes,spec}/**/*') +
+ %w(MIT-LICENSE README.textile)
s.homepage = 'http://github.com/collectiveidea/delayed_job'
s.rdoc_options = ["--main", "README.textile", "--inline-source", "--line-numbers"]
s.require_paths = ["lib"]
View
22 generators/delayed_job/delayed_job_generator.rb
@@ -1,22 +0,0 @@
-class DelayedJobGenerator < Rails::Generator::Base
- default_options :skip_migration => false
-
- def manifest
- record do |m|
- m.template 'script', 'script/delayed_job', :chmod => 0755
- if !options[:skip_migration] && defined?(ActiveRecord)
- m.migration_template "migration.rb", 'db/migrate',
- :migration_file_name => "create_delayed_jobs"
- end
- end
- end
-
-protected
-
- def add_options!(opt)
- opt.separator ''
- opt.separator 'Options:'
- opt.on("--skip-migration", "Don't generate a migration") { |v| options[:skip_migration] = v }
- end
-
-end
View
21 generators/delayed_job/templates/migration.rb
@@ -1,21 +0,0 @@
-class CreateDelayedJobs < ActiveRecord::Migration
- def self.up
- create_table :delayed_jobs, :force => true do |table|
- table.integer :priority, :default => 0 # Allows some jobs to jump to the front of the queue
- table.integer :attempts, :default => 0 # Provides for retries, but still fail eventually.
- table.text :handler # YAML-encoded string of the object that will do work
- table.text :last_error # reason for last failure (See Note below)
- table.datetime :run_at # When to run. Could be Time.zone.now for immediately, or sometime in the future.
- table.datetime :locked_at # Set when a client is working on this object
- table.datetime :failed_at # Set when all retries have failed (actually, by default, the record is deleted instead)
- table.string :locked_by # Who is working on this object (if locked)
- table.timestamps
- end
-
- add_index :delayed_jobs, [:priority, :run_at], :name => 'delayed_jobs_priority'
- end
-
- def self.down
- drop_table :delayed_jobs
- end
-end
View
5 generators/delayed_job/templates/script
@@ -1,5 +0,0 @@
-#!/usr/bin/env ruby
-
-require File.expand_path(File.join(File.dirname(__FILE__), '..', 'config', 'environment'))
-require 'delayed/command'
-Delayed::Command.new(ARGV).daemonize
View
1  init.rb
@@ -1 +0,0 @@
-require File.join(File.dirname(__FILE__), 'rails', 'init')
View
15 lib/delayed/backend/active_record.rb
@@ -25,17 +25,10 @@ class Job < ::ActiveRecord::Base
before_save :set_default_run_at
- if ::ActiveRecord::VERSION::MAJOR >= 3
- scope :ready_to_run, lambda {|worker_name, max_run_time|
- where(['(run_at <= ? AND (locked_at IS NULL OR locked_at < ?) OR locked_by = ?) AND failed_at IS NULL', db_time_now, db_time_now - max_run_time, worker_name])
- }
- scope :by_priority, order('priority ASC, run_at ASC')
- else
- named_scope :ready_to_run, lambda {|worker_name, max_run_time|
- {:conditions => ['(run_at <= ? AND (locked_at IS NULL OR locked_at < ?) OR locked_by = ?) AND failed_at IS NULL', db_time_now, db_time_now - max_run_time, worker_name]}
- }
- named_scope :by_priority, :order => 'priority ASC, run_at ASC'
- end
+ scope :ready_to_run, lambda {|worker_name, max_run_time|
+ where(['(run_at <= ? AND (locked_at IS NULL OR locked_at < ?) OR locked_by = ?) AND failed_at IS NULL', db_time_now, db_time_now - max_run_time, worker_name])
+ }
+ scope :by_priority, order('priority ASC, run_at ASC')
def self.after_fork
::ActiveRecord::Base.connection.reconnect!
View
47 lib/delayed/backend/base.rb
@@ -7,7 +7,7 @@ module Base
def self.included(base)
base.extend ClassMethods
end
-
+
module ClassMethods
# Add a job to the queue
def enqueue(*args)
@@ -15,26 +15,26 @@ def enqueue(*args)
unless object.respond_to?(:perform)
raise ArgumentError, 'Cannot enqueue items which do not respond to perform'
end
-
+
priority = args.first || Delayed::Worker.default_priority
run_at = args[1]
self.create(:payload_object => object, :priority => priority.to_i, :run_at => run_at)
end
-
+
# Hook method that is called before a new worker is forked
def before_fork
end
-
+
# Hook method that is called after a new worker is forked
def after_fork
end
-
+
def work_off(num = 100)
warn "[DEPRECATION] `Delayed::Job.work_off` is deprecated. Use `Delayed::Worker.new.work_off instead."
Delayed::Worker.new.work_off(num)
end
end
-
+
ParseObjectFromYaml = /\!ruby\/\w+\:([^\s]+)/
def failed?
@@ -52,7 +52,7 @@ def name
def payload_object=(object)
self.handler = object.to_yaml
end
-
+
def payload_object
@payload_object ||= YAML.load(self.handler)
rescue TypeError, LoadError, NameError => e
@@ -60,32 +60,37 @@ def payload_object
"Job failed to load: #{e.message}. Try to manually require the required file. Handler: #{handler.inspect}"
end
- # Moved into its own method so that new_relic can trace it.
def invoke_job
- payload_object.before(self) if payload_object.respond_to?(:before)
- begin
- payload_object.perform
- payload_object.success(self) if payload_object.respond_to?(:success)
- rescue Exception => e
- payload_object.failure(self, e) if payload_object.respond_to?(:failure)
- raise e
- ensure
- payload_object.after(self) if payload_object.respond_to?(:after)
- end
+ hook :before
+ payload_object.perform
+ hook :success
+ rescue Exception => e
+ hook :error, e
+ raise e
+ ensure
+ hook :after
end
-
+
# Unlock this job (note: not saved to DB)
def unlock
self.locked_at = nil
self.locked_by = nil
end
-
+
+ def hook(name, *args)
+ if payload_object.respond_to?(name)
+ method = payload_object.method(name)
+ args.unshift(self)
+ method.call(*args.slice(0, method.arity))
+ end
+ end
+
protected
def set_default_run_at
self.run_at ||= self.class.db_time_now
end
-
+
end
end
end
View
41 lib/delayed/backend/shared_spec.rb
@@ -57,22 +57,29 @@ def create_job(opts = {})
describe "callbacks" do
before(:each) do
- SuccessfulCallbackJob.messages = []
- FailureCallbackJob.messages = []
+ CallbackJob.messages = []
end
it "should call before and after callbacks" do
- job = described_class.enqueue(SuccessfulCallbackJob.new)
+ job = described_class.enqueue(CallbackJob.new)
job.invoke_job
- SuccessfulCallbackJob.messages.should == ["before perform", "perform", "success!", "after perform"]
+ CallbackJob.messages.should == ["before", "perform", "success", "after"]
end
it "should call the after callback with an error" do
- job = described_class.enqueue(FailureCallbackJob.new)
- lambda {job.invoke_job}.should raise_error
- FailureCallbackJob.messages.should == ["before perform", "error: RuntimeError", "after perform"]
+ job = described_class.enqueue(CallbackJob.new)
+ job.payload_object.should_receive(:perform).and_raise(RuntimeError.new("fail"))
+
+ lambda { job.invoke_job }.should raise_error
+ CallbackJob.messages.should == ["before", "error: RuntimeError", "after"]
end
+ it "should call error when before raises an error" do
+ job = described_class.enqueue(CallbackJob.new)
+ job.payload_object.should_receive(:before).and_raise(RuntimeError.new("fail"))
+ lambda { job.invoke_job }.should raise_error(RuntimeError)
+ CallbackJob.messages.should == ["error: RuntimeError", "after"]
+ end
end
describe "payload_object" do
@@ -394,33 +401,33 @@ def create_job(opts = {})
end
share_examples_for "any failure more than Worker.max_attempts times" do
- context "when the job's payload has an #on_permanent_failure hook" do
+ context "when the job's payload has a #failure hook" do
before do
@job = Delayed::Job.create :payload_object => OnPermanentFailureJob.new
- @job.payload_object.should respond_to :on_permanent_failure
+ @job.payload_object.should respond_to :failure
end
it "should run that hook" do
- @job.payload_object.should_receive :on_permanent_failure
+ @job.payload_object.should_receive :failure
Delayed::Worker.max_attempts.times { @worker.reschedule(@job) }
end
end
- context "when the job's payload has no #on_permanent_failure hook" do
+ context "when the job's payload has no #failure hook" do
# It's a little tricky to test this in a straightforward way,
# because putting a should_not_receive expectation on
- # @job.payload_object.on_permanent_failure makes that object
+ # @job.payload_object.failure makes that object
# incorrectly return true to
- # payload_object.respond_to? :on_permanent_failure, which is what
- # reschedule uses to decide whether to call on_permanent_failure.
+ # payload_object.respond_to? :failure, which is what
+ # reschedule uses to decide whether to call failure.
# So instead, we just make sure that the payload_object as it
- # already stands doesn't respond_to? on_permanent_failure, then
+ # already stands doesn't respond_to? failure, then
# shove it through the iterated reschedule loop and make sure we
# don't get a NoMethodError (caused by calling that nonexistent
- # on_permanent_failure method).
+ # failure method).
before do
- @job.payload_object.should_not respond_to(:on_permanent_failure)
+ @job.payload_object.should_not respond_to(:failure)
end
it "should not try to run that hook" do
View
7 lib/delayed/message_sending.rb
@@ -3,14 +3,15 @@
module Delayed
class DelayProxy < ActiveSupport::BasicObject
- def initialize(target, options)
+ def initialize(payload_class, target, options)
+ @payload_class = payload_class
@target = target
@options = options
end
def method_missing(method, *args)
Job.create({
- :payload_object => PerformableMethod.new(@target, method.to_sym, args),
+ :payload_object => @payload_class.new(@target, method.to_sym, args),
:priority => ::Delayed::Worker.default_priority
}.merge(@options))
end
@@ -18,7 +19,7 @@ def method_missing(method, *args)
module MessageSending
def delay(options = {})
- DelayProxy.new(self, options)
+ DelayProxy.new(PerformableMethod, self, options)
end
alias __delay__ delay
View
21 lib/delayed/performable_mailer.rb
@@ -0,0 +1,21 @@
+require 'action_mailer'
+
+module Delayed
+ class PerformableMailer < PerformableMethod
+ def perform
+ object.send(method, *args).deliver
+ end
+ end
+end
+
+ActionMailer::Base.class_eval do
+ def self.delay(options = {})
+ Delayed::DelayProxy.new(Delayed::PerformableMailer, self, options)
+ end
+end
+
+Mail::Message.class_eval do
+ def delay(*args)
+ raise RuntimeError, "Use MyMailer.delay.mailer_action(args) to delay sending of emails."
+ end
+end
View
15 lib/delayed/worker.rb
@@ -42,7 +42,7 @@ def self.guess_backend
end
def initialize(options={})
- @quiet = options[:quiet]
+ @quiet = options.has_key?(:quiet) ? options[:quiet] : true
self.class.min_priority = options[:min_priority] if options.has_key?(:min_priority)
self.class.max_priority = options[:max_priority] if options.has_key?(:max_priority)
end
@@ -134,17 +134,10 @@ def reschedule(job, time = nil)
job.save!
else
say "PERMANENTLY removing #{job.name} because of #{job.attempts} consecutive failures.", Logger::INFO
-
- if job.payload_object.respond_to? :on_permanent_failure
- say "Running on_permanent_failure hook"
- failure_method = job.payload_object.method(:on_permanent_failure)
- if failure_method.arity == 1
- failure_method.call(job)
- else
- failure_method.call
- end
+ if job.respond_to?(:on_permanent_failure)
+ warn "[DEPRECATION] The #on_permanent_failure hook has been renamed to #failure."
end
-
+ job.hook(:failure)
self.class.destroy_failed_jobs ? job.destroy : job.update_attributes(:failed_at => Delayed::Job.db_time_now)
end
end
View
3  lib/delayed_job.rb
@@ -2,10 +2,11 @@
require File.dirname(__FILE__) + '/delayed/message_sending'
require File.dirname(__FILE__) + '/delayed/performable_method'
+require File.dirname(__FILE__) + '/delayed/performable_mailer' if defined?(ActionMailer)
require File.dirname(__FILE__) + '/delayed/yaml_ext'
require File.dirname(__FILE__) + '/delayed/backend/base'
require File.dirname(__FILE__) + '/delayed/worker'
-require File.dirname(__FILE__) + '/delayed/railtie' if defined?(::Rails::Railtie)
+require File.dirname(__FILE__) + '/delayed/railtie' if defined?(Rails::Railtie)
Object.send(:include, Delayed::MessageSending)
Module.send(:include, Delayed::MessageSending::ClassMethods)
View
5 rails/init.rb
@@ -1,5 +0,0 @@
-require 'delayed_job'
-
-config.after_initialize do
- Delayed::Worker.guess_backend
-end
View
46 spec/performable_mailer_spec.rb
@@ -0,0 +1,46 @@
+require 'spec_helper'
+
+require 'action_mailer'
+class MyMailer < ActionMailer::Base
+ def signup(email)
+ mail :to => email, :subject => "Delaying Emails"
+ end
+end
+
+describe ActionMailer::Base do
+ describe "delay" do
+ it "should enqueue a PerformableEmail job" do
+ lambda {
+ job = MyMailer.delay.signup('john@example.com')
+ job.payload_object.class.should == Delayed::PerformableMailer
+ job.payload_object.method.should == :signup
+ job.payload_object.args.should == ['john@example.com']
+ }.should change { Delayed::Job.count }.by(1)
+ end
+ end
+
+ describe "delay on a mail object" do
+ it "should raise an exception" do
+ lambda {
+ MyMailer.signup('john@example.com').delay
+ }.should raise_error(RuntimeError)
+ end
+ end
+
+ describe Delayed::PerformableMailer do
+ describe "perform" do
+ before do
+ @email = mock('email', :deliver => true)
+ @mailer_class = mock('MailerClass', :signup => @email)
+ @mailer = Delayed::PerformableMailer.new(@mailer_class, :signup, ['john@example.com'])
+ end
+
+ it "should call the method and #deliver on the mailer" do
+ @mailer_class.should_receive(:signup).with('john@example.com')
+ @email.should_receive(:deliver)
+ @mailer.perform
+ end
+ end
+ end
+
+end
View
8 spec/performable_method_spec.rb
@@ -21,11 +21,11 @@
@method.perform
end
- it "should respond to on_permanent_failure when implemented and target object is called via object.delay.do_something" do
+ it "should respond to failure when implemented and target object is called via object.delay.do_something" do
@method = Delayed::PerformableMethod.new(OnPermanentFailureJob.new, :perform, [])
- @method.respond_to?(:on_permanent_failure).should be_true
- @method.object.should_receive(:on_permanent_failure)
- @method.on_permanent_failure
+ @method.respond_to?(:failure).should be_true
+ @method.object.should_receive(:failure)
+ @method.failure
end
end
View
24 spec/sample_jobs.rb
@@ -19,7 +19,7 @@ def perform; sleep 250; end
end
class OnPermanentFailureJob < SimpleJob
- def on_permanent_failure
+ def failure
end
end
@@ -30,32 +30,30 @@ def perform; @@runs += 1; end
end
end
-class SuccessfulCallbackJob
+class CallbackJob
cattr_accessor :messages
def before(job)
- SuccessfulCallbackJob.messages << 'before perform'
+ self.class.messages << 'before'
end
def perform
- SuccessfulCallbackJob.messages << 'perform'
+ self.class.messages << 'perform'
end
def after(job, error = nil)
- SuccessfulCallbackJob.messages << 'after perform'
+ self.class.messages << 'after'
end
def success(job)
- SuccessfulCallbackJob.messages << 'success!'
+ self.class.messages << 'success'
end
-
- def failure(job, error)
- SuccessfulCallbackJob.messages << "error: #{error.class}"
+
+ def error(job, error)
+ self.class.messages << "error: #{error.class}"
end
-end
-class FailureCallbackJob < SuccessfulCallbackJob
- def perform
- raise "failure job"
+ def failure(job)
+ self.class.messages << 'failure'
end
end
View
4 spec/spec_helper.rb
@@ -5,13 +5,11 @@
require 'spec'
require 'logger'
-gem 'activerecord', ENV['RAILS_VERSION'] if ENV['RAILS_VERSION']
-
require 'delayed_job'
require 'delayed/backend/shared_spec'
Delayed::Worker.logger = Logger.new('/tmp/dj.log')
-RAILS_ENV = 'test'
+ENV['RAILS_ENV'] = 'test'
require 'active_record'
View
1  tasks/jobs.rake
@@ -1 +0,0 @@
-require File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib', 'delayed', 'tasks'))
Please sign in to comment.
Something went wrong with that request. Please try again.