spec and migration cleanup for delayed jobs

Fixes up some abstraction so that we can add other jobs backends and
specs, migrations, etc will work as expected.

Also remove some unused parameters from the Delayed::Job methods for
finding and locking jobs.

test plan: run the database migrations on a new db, and migrations
should work without error. delayed jobs should also be created and
processed by workers without error.

Change-Id: I1fe6ef5464f9780db3010fa002703fc030832f8d
Reviewed-on: https://gerrit.instructure.com/11590
Reviewed-by: Cody Cutrer <cody@instructure.com>
Tested-by: Jenkins <jenkins@instructure.com>
Reviewed-by: Bracken Mosbacker <bracken@instructure.com>
This commit is contained in:
Brian Palmer 2012-06-11 13:17:31 -06:00
parent 73323a4815
commit 8e9bf96d18
23 changed files with 243 additions and 259 deletions

View File

@ -1,6 +1,6 @@
class CleanupDelayedJobsIndexes < ActiveRecord::Migration class CleanupDelayedJobsIndexes < ActiveRecord::Migration
def self.connection def self.connection
Delayed::Job.connection Delayed::Backend::ActiveRecord::Job.connection
end end
def self.up def self.up

View File

@ -29,32 +29,12 @@ class OptimizeDelayedJobs < ActiveRecord::Migration
add_index :delayed_jobs, %w(strand id), :name => 'index_delayed_jobs_on_strand' add_index :delayed_jobs, %w(strand id), :name => 'index_delayed_jobs_on_strand'
# move all failed jobs to the new failed table # move all failed jobs to the new failed table
Delayed::Job.find_each(:conditions => 'failed_at is not null') do |job| Delayed::Backend::ActiveRecord::Job.find_each(:conditions => 'failed_at is not null') do |job|
job.fail! unless job.on_hold? job.fail! unless job.on_hold?
end end
end end
def self.down def self.down
remove_index :delayed_jobs, :name => 'index_delayed_jobs_for_get_next' raise ActiveRecord::IrreversibleMigration
remove_index :delayed_jobs, :name => 'index_delayed_jobs_on_strand'
add_index :delayed_jobs, [:strand]
# from CleanupDelayedJobsIndexes migration
case connection.adapter_name
when 'PostgreSQL'
# "nulls first" syntax is postgresql specific, and allows for more
# efficient querying for the next job
connection.execute("CREATE INDEX get_delayed_jobs_index ON delayed_jobs (priority, run_at, failed_at nulls first, locked_at nulls first, queue)")
else
add_index :delayed_jobs, %w(priority run_at locked_at failed_at queue), :name => 'get_delayed_jobs_index'
end
Delayed::Job::Failed.find_each do |job|
attrs = job.attributes
attrs.delete('id')
Delayed::Job.create!(attrs)
end
drop_table :failed_jobs
end end
end end

View File

@ -1,6 +1,6 @@
class RemoveInactiveEnrollmentState < ActiveRecord::Migration class RemoveInactiveEnrollmentState < ActiveRecord::Migration
def self.up def self.up
Delayed::Job.delete_all(:tag => 'EnrollmentDateRestrictions.update_restricted_enrollments') Delayed::Backend::ActiveRecord::Job.delete_all(:tag => 'EnrollmentDateRestrictions.update_restricted_enrollments')
Enrollment.update_all({:workflow_state => 'active'}, :workflow_state => 'inactive') Enrollment.update_all({:workflow_state => 'active'}, :workflow_state => 'inactive')
end end

View File

@ -1,6 +1,6 @@
class AddDelayedJobsNextInStrand < ActiveRecord::Migration class AddDelayedJobsNextInStrand < ActiveRecord::Migration
def self.connection def self.connection
Delayed::Job.connection Delayed::Backend::ActiveRecord::Job.connection
end end
def self.up def self.up
@ -95,10 +95,10 @@ class AddDelayedJobsNextInStrand < ActiveRecord::Migration
if connection.adapter_name == 'MySQL' if connection.adapter_name == 'MySQL'
# use temp tables to work around subselect limitations in mysql # use temp tables to work around subselect limitations in mysql
execute(%{CREATE TEMPORARY TABLE dj_20110831210257 (strand varchar(255), next_job_id bigint) SELECT strand, min(id) as next_job_id FROM delayed_jobs WHERE strand IS NOT NULL GROUP BY strand}) execute(%{CREATE TEMPORARY TABLE dj_20110831210257 (strand varchar(255), next_job_id bigint) SELECT strand, min(id) as next_job_id FROM delayed_jobs WHERE strand IS NOT NULL GROUP BY strand})
execute(%{UPDATE delayed_jobs SET next_in_strand = #{Delayed::Job.quote_value(false)} WHERE strand IS NOT NULL AND id <> (SELECT t.next_job_id FROM dj_20110831210257 t WHERE t.strand = delayed_jobs.strand)}) execute(%{UPDATE delayed_jobs SET next_in_strand = #{Delayed::Backend::ActiveRecord::Job.quote_value(false)} WHERE strand IS NOT NULL AND id <> (SELECT t.next_job_id FROM dj_20110831210257 t WHERE t.strand = delayed_jobs.strand)})
execute(%{DROP TABLE dj_20110831210257}) execute(%{DROP TABLE dj_20110831210257})
else else
execute(%{UPDATE delayed_jobs SET next_in_strand = #{Delayed::Job.quote_value(false)} WHERE strand IS NOT NULL AND id <> (SELECT id FROM delayed_jobs j2 WHERE j2.strand = delayed_jobs.strand ORDER BY j2.strand, j2.id ASC LIMIT 1)}) execute(%{UPDATE delayed_jobs SET next_in_strand = #{Delayed::Backend::ActiveRecord::Job.quote_value(false)} WHERE strand IS NOT NULL AND id <> (SELECT id FROM delayed_jobs j2 WHERE j2.strand = delayed_jobs.strand ORDER BY j2.strand, j2.id ASC LIMIT 1)})
end end
end end

View File

@ -2,7 +2,7 @@ class DelayedJobsDeleteTriggerLockForUpdate < ActiveRecord::Migration
tag :predeploy tag :predeploy
def self.connection def self.connection
Delayed::Job.connection Delayed::Backend::ActiveRecord::Job.connection
end end
def self.up def self.up

View File

@ -2,7 +2,7 @@ class DelayedJobsUseAdvisoryLocks < ActiveRecord::Migration
tag :predeploy tag :predeploy
def self.connection def self.connection
Delayed::Job.connection Delayed::Backend::ActiveRecord::Job.connection
end end
def self.up def self.up

View File

@ -4,7 +4,7 @@ class IndexJobsOnLockedBy < ActiveRecord::Migration
self.transactional = false self.transactional = false
def self.connection def self.connection
Delayed::Job.connection Delayed::Backend::ActiveRecord::Job.connection
end end
def self.up def self.up

View File

@ -4,7 +4,7 @@ class AddJobsRunAtIndex < ActiveRecord::Migration
self.transactional = false self.transactional = false
def self.connection def self.connection
Delayed::Job.connection Delayed::Backend::ActiveRecord::Job.connection
end end
def self.up def self.up

View File

@ -194,7 +194,7 @@ describe "site admin jobs ui" do
context "running jobs" do context "running jobs" do
it "should display running jobs in the workers grid" do it "should display running jobs in the workers grid" do
j = Delayed::Job.first(:order => :id) j = Delayed::Job.first(:order => :id)
j.lock_exclusively!(100, 'my test worker') j.lock_exclusively!('my test worker')
load_jobs_page load_jobs_page
ffj('#running-grid .slick-row').size.should eql 1 ffj('#running-grid .slick-row').size.should eql 1
first_cell = f('#running-grid .slick-cell.l0.r0') first_cell = f('#running-grid .slick-cell.l0.r0')

View File

@ -32,7 +32,7 @@ ALL_MODELS = (ActiveRecord::Base.send(:subclasses) +
model = File.basename(file, ".*").camelize.constantize model = File.basename(file, ".*").camelize.constantize
next unless model < ActiveRecord::Base next unless model < ActiveRecord::Base
model model
}).compact.uniq.reject { |model| model.superclass != ActiveRecord::Base || model == Tableless } }).compact.uniq.reject { |model| model.superclass != ActiveRecord::Base || (model.respond_to?(:tableless?) && model.tableless?) }
ALL_MODELS << Version ALL_MODELS << Version
ALL_MODELS << Delayed::Backend::ActiveRecord::Job::Failed ALL_MODELS << Delayed::Backend::ActiveRecord::Job::Failed
ALL_MODELS << Delayed::Backend::ActiveRecord::Job ALL_MODELS << Delayed::Backend::ActiveRecord::Job
@ -759,7 +759,6 @@ Spec::Runner.configure do |config|
def run_jobs def run_jobs
while job = Delayed::Job.get_and_lock_next_available( while job = Delayed::Job.get_and_lock_next_available(
'spec run_jobs', 'spec run_jobs',
1.hour,
Delayed::Worker.queue, Delayed::Worker.queue,
0, 0,
Delayed::MAX_PRIORITY) Delayed::MAX_PRIORITY)

View File

@ -19,11 +19,6 @@ module Delayed
class Job < ::ActiveRecord::Base class Job < ::ActiveRecord::Base
include Delayed::Backend::Base include Delayed::Backend::Base
set_table_name :delayed_jobs set_table_name :delayed_jobs
attr_writer :current_shard
def current_shard
@current_shard || Shard.default
end
# be aware that some strand functionality is controlled by triggers on # be aware that some strand functionality is controlled by triggers on
# the database. see # the database. see
@ -68,9 +63,6 @@ module Delayed
end end
end end
cattr_accessor :default_priority
self.default_priority = Delayed::NORMAL_PRIORITY
named_scope :current, lambda { named_scope :current, lambda {
{ :conditions => ["run_at <= ?", db_time_now] } { :conditions => ["run_at <= ?", db_time_now] }
} }
@ -88,7 +80,7 @@ module Delayed
# 500.times { |i| "ohai".send_later_enqueue_args(:reverse, { :run_at => (12.hours.ago + (rand(24.hours.to_i))) }) } # 500.times { |i| "ohai".send_later_enqueue_args(:reverse, { :run_at => (12.hours.ago + (rand(24.hours.to_i))) }) }
# then fire up your workers # then fire up your workers
# you can check out strand correctness: diff test1.txt <(sort -n test1.txt) # you can check out strand correctness: diff test1.txt <(sort -n test1.txt)
named_scope :ready_to_run, lambda {|worker_name, max_run_time| named_scope :ready_to_run, lambda {
{ :conditions => ["run_at <= ? AND locked_at IS NULL AND next_in_strand = ?", db_time_now, true] } { :conditions => ["run_at <= ? AND locked_at IS NULL AND next_in_strand = ?", db_time_now, true] }
} }
named_scope :by_priority, :order => 'priority ASC, run_at ASC' named_scope :by_priority, :order => 'priority ASC, run_at ASC'
@ -103,7 +95,6 @@ module Delayed
end end
def self.get_and_lock_next_available(worker_name, def self.get_and_lock_next_available(worker_name,
max_run_time,
queue = nil, queue = nil,
min_priority = nil, min_priority = nil,
max_priority = nil) max_priority = nil)
@ -113,30 +104,26 @@ module Delayed
@batch_size ||= Setting.get_cached('jobs_get_next_batch_size', '5').to_i @batch_size ||= Setting.get_cached('jobs_get_next_batch_size', '5').to_i
loop do loop do
jobs = find_available(worker_name, @batch_size, max_run_time, queue, min_priority, max_priority) jobs = find_available(@batch_size, queue, min_priority, max_priority)
return nil if jobs.empty? return nil if jobs.empty?
job = jobs.detect do |job| job = jobs.detect do |job|
job.lock_exclusively!(max_run_time, worker_name) job.lock_exclusively!(worker_name)
end end
return job if job return job if job
end end
end end
def self.find_available(worker_name, def self.find_available(limit,
limit,
max_run_time,
queue = nil, queue = nil,
min_priority = nil, min_priority = nil,
max_priority = nil) max_priority = nil)
all_available(worker_name, max_run_time, queue, min_priority, max_priority).all(:limit => limit) all_available(queue, min_priority, max_priority).all(:limit => limit)
end end
def self.all_available(worker_name, def self.all_available(queue = nil,
max_run_time,
queue = nil,
min_priority = nil, min_priority = nil,
max_priority = nil) max_priority = nil)
scope = self.ready_to_run(worker_name, max_run_time) scope = self.ready_to_run
scope = scope.scoped(:conditions => ['priority >= ?', min_priority]) if min_priority scope = scope.scoped(:conditions => ['priority >= ?', min_priority]) if min_priority
scope = scope.scoped(:conditions => ['priority <= ?', max_priority]) if max_priority scope = scope.scoped(:conditions => ['priority <= ?', max_priority]) if max_priority
scope = scope.scoped(:conditions => ['queue = ?', queue]) if queue scope = scope.scoped(:conditions => ['queue = ?', queue]) if queue
@ -169,7 +156,7 @@ module Delayed
# It's important to note that for performance reasons, this method does # It's important to note that for performance reasons, this method does
# not re-check the strand constraints -- so you could manually lock a # not re-check the strand constraints -- so you could manually lock a
# job using this method that isn't the next to run on its strand. # job using this method that isn't the next to run on its strand.
def lock_exclusively!(max_run_time, worker) def lock_exclusively!(worker)
now = self.class.db_time_now now = self.class.db_time_now
# We don't own this job so we will update the locked_by name and the locked_at # We don't own this job so we will update the locked_by name and the locked_at
affected_rows = self.class.update_all(["locked_at = ?, locked_by = ?", now, worker], ["id = ? and locked_at is null and run_at <= ?", id, now]) affected_rows = self.class.update_all(["locked_at = ?, locked_by = ?", now, worker], ["id = ? and locked_at is null and run_at <= ?", id, now])
@ -224,13 +211,6 @@ module Delayed
update_all(["locked_by = NULL, locked_at = NULL, attempts = 0, run_at = (CASE WHEN run_at > ? THEN run_at ELSE ? END), failed_at = NULL", now, now]) update_all(["locked_by = NULL, locked_at = NULL, attempts = 0, run_at = (CASE WHEN run_at > ? THEN run_at ELSE ? END), failed_at = NULL", now, now])
end end
# Get the current time (GMT or local depending on DB)
# Note: This does not ping the DB to get the time, so all your clients
# must have syncronized clocks.
def self.db_time_now
Time.now.in_time_zone
end
class Failed < Job class Failed < Job
include Delayed::Backend::Base include Delayed::Backend::Base
set_table_name :failed_jobs set_table_name :failed_jobs

View File

@ -12,10 +12,17 @@ module Delayed
def self.included(base) def self.included(base)
base.extend ClassMethods base.extend ClassMethods
base.send :attr_writer, :current_shard
base.default_priority = Delayed::NORMAL_PRIORITY
end
def current_shard
@current_shard || Shard.default
end end
module ClassMethods module ClassMethods
attr_accessor :batches attr_accessor :batches
attr_accessor :default_priority
# Add a job to the queue # Add a job to the queue
# The first argument should be an object that respond_to?(:perform) # The first argument should be an object that respond_to?(:perform)
@ -61,6 +68,13 @@ module Delayed
def in_delayed_job=(val) def in_delayed_job=(val)
Thread.current[:in_delayed_job] = val Thread.current[:in_delayed_job] = val
end end
# Get the current time (GMT or local depending on DB)
# Note: This does not ping the DB to get the time, so all your clients
# must have syncronized clocks.
def db_time_now
Time.now.in_time_zone
end
end end
def failed? def failed?

View File

@ -86,7 +86,6 @@ class Worker
job = Delayed::Job.get_and_lock_next_available( job = Delayed::Job.get_and_lock_next_available(
name, name,
self.class.max_run_time,
queue, queue,
min_priority, min_priority,
max_priority) max_priority)

View File

@ -0,0 +1,29 @@
require File.expand_path("../spec_helper", __FILE__)
describe 'Delayed::Backed::ActiveRecord::Job' do
before :all do
@job_spec_backend = Delayed::Job
Delayed.send(:remove_const, :Job)
Delayed::Job = Delayed::Backend::ActiveRecord::Job
end
after :all do
Delayed.send(:remove_const, :Job)
Delayed::Job = @job_spec_backend
end
before do
Delayed::Job.delete_all
end
it_should_behave_like 'a delayed_jobs implementation'
it "should recover as well as possible from a failure failing a job" do
Delayed::Job::Failed.stubs(:create).raises(RuntimeError)
job = "test".send_later :reverse
job_id = job.id
proc { job.fail! }.should raise_error
proc { Delayed::Job.find(job_id) }.should raise_error(ActiveRecord::RecordNotFound)
Delayed::Job.count.should == 0
end
end

View File

@ -1,6 +1,6 @@
require File.expand_path("../../../../../spec/sharding_spec_helper", __FILE__) require File.expand_path("../../../../../spec/sharding_spec_helper", __FILE__)
describe Delayed::Batch do shared_examples_for 'Delayed::Batch' do
before :each do before :each do
Delayed::Worker.queue = nil Delayed::Worker.queue = nil
Delayed::Job.delete_all Delayed::Job.delete_all

View File

@ -1,10 +1,10 @@
require File.expand_path("../spec_helper", __FILE__) shared_examples_for 'random ruby objects' do
def set_queue(name)
describe 'random ruby objects' do old_name = Delayed::Worker.queue
before :each do Delayed::Worker.queue = name
Delayed::Worker.queue = nil ensure
Delayed::Job.delete_all Delayed::Worker.queue = old_name
end end
it "should respond_to :send_later method" do it "should respond_to :send_later method" do
Object.new.respond_to?(:send_later) Object.new.respond_to?(:send_later)
@ -281,13 +281,10 @@ describe 'random ruby objects' do
it "should call send later on methods which are wrapped with handle_asynchronously" do it "should call send later on methods which are wrapped with handle_asynchronously" do
story = Story.create :text => 'Once upon...' story = Story.create :text => 'Once upon...'
Delayed::Job.count.should == 0 job = nil
expect { job = story.whatever(1, 5) }.to change(Delayed::Job, :count).by(1)
story.whatever(1, 5)
Delayed::Job.count.should == 1
job = Delayed::Job.find(:first)
job.payload_object.class.should == Delayed::PerformableMethod job.payload_object.class.should == Delayed::PerformableMethod
job.payload_object.method.should == :whatever_without_send_later job.payload_object.method.should == :whatever_without_send_later
job.payload_object.args.should == [1, 5] job.payload_object.args.should == [1, 5]
@ -296,29 +293,29 @@ describe 'random ruby objects' do
it "should call send later on methods which are wrapped with handle_asynchronously_with_queue" do it "should call send later on methods which are wrapped with handle_asynchronously_with_queue" do
story = Story.create :text => 'Once upon...' story = Story.create :text => 'Once upon...'
Delayed::Job.count.should == 0 job = nil
expect { job = story.whatever_else(1, 5) }.to change(Delayed::Job, :count).by(1)
story.whatever_else(1, 5)
Delayed::Job.count.should == 1
job = Delayed::Job.find(:first)
job.payload_object.class.should == Delayed::PerformableMethod job.payload_object.class.should == Delayed::PerformableMethod
job.payload_object.method.should == :whatever_else_without_send_later job.payload_object.method.should == :whatever_else_without_send_later
job.payload_object.args.should == [1, 5] job.payload_object.args.should == [1, 5]
job.payload_object.perform.should == 'Once upon...' job.payload_object.perform.should == 'Once upon...'
end end
context "send_later" do context "send_later" do
it "should use the default queue if there is one" do it "should use the default queue if there is one" do
Delayed::Worker.queue = "testqueue" set_queue("testqueue") do
job = "string".send_later :reverse job = "string".send_later :reverse
job.queue.should == "testqueue" job.queue.should == "testqueue"
end
end end
it "should have nil queue if there is not a default" do it "should have nil queue if there is not a default" do
job = "string".send_later :reverse set_queue(nil) do
job.queue.should == nil job = "string".send_later :reverse
job.queue.should == nil
end
end end
end end
@ -344,14 +341,17 @@ describe 'random ruby objects' do
end end
it "should use the default queue if there is one" do it "should use the default queue if there is one" do
Delayed::Worker.queue = "testqueue" set_queue("testqueue") do
job = "string".send_at 1.hour.from_now, :reverse job = "string".send_at 1.hour.from_now, :reverse
job.queue.should == "testqueue" job.queue.should == "testqueue"
end
end end
it "should have nil queue if there is not a default" do it "should have nil queue if there is not a default" do
job = "string".send_at 1.hour.from_now, :reverse set_queue(nil) do
job.queue.should == nil job = "string".send_at 1.hour.from_now, :reverse
job.queue.should == nil
end
end end
end end
@ -369,9 +369,10 @@ describe 'random ruby objects' do
end end
it "should override the default queue" do it "should override the default queue" do
Delayed::Worker.queue = "default_queue" set_queue("default_queue") do
job = "string".send_at_with_queue(1.hour.from_now, :length, "testqueue") job = "string".send_at_with_queue(1.hour.from_now, :length, "testqueue")
job.queue.should == "testqueue" job.queue.should == "testqueue"
end
end end
it "should store payload as PerformableMethod" do it "should store payload as PerformableMethod" do

View File

@ -1,33 +0,0 @@
require File.expand_path("../spec_helper", __FILE__)
describe Delayed::Job do
before(:all) do
@backend = Delayed::Job
end
before(:each) do
Delayed::Job.delete_all
SimpleJob.runs = 0
end
it_should_behave_like 'a backend'
it "should fail on job creation if an unsaved AR object is used" do
story = Story.new :text => "Once upon..."
lambda { story.send_later(:text) }.should raise_error
reader = StoryReader.new
lambda { reader.send_later(:read, story) }.should raise_error
lambda { [story, 1, story, false].send_later(:first) }.should raise_error
end
it "should recover as well as possible from a failure failing a job" do
Delayed::Job::Failed.stubs(:create).raises(RuntimeError)
job = "test".send_later :reverse
job_id = job.id
proc { job.fail! }.should raise_error
proc { Delayed::Job.find(job_id) }.should raise_error(ActiveRecord::RecordNotFound)
Delayed::Job.count.should == 0
end
end

View File

@ -1,6 +1,4 @@
require File.expand_path("../spec_helper", __FILE__) shared_examples_for 'Delayed::PerformableMethod' do
describe Delayed::PerformableMethod do
it "should not ignore ActiveRecord::RecordNotFound errors because they are not always permanent" do it "should not ignore ActiveRecord::RecordNotFound errors because they are not always permanent" do
story = Story.create :text => 'Once upon...' story = Story.create :text => 'Once upon...'

View File

@ -1,79 +1,79 @@
shared_examples_for 'a backend' do shared_examples_for 'a backend' do
def create_job(opts = {}) def create_job(opts = {})
@backend.enqueue(SimpleJob.new, { :queue => nil }.merge(opts)) Delayed::Job.enqueue(SimpleJob.new, { :queue => nil }.merge(opts))
end end
before do before do
SimpleJob.runs = 0 SimpleJob.runs = 0
end end
it "should set run_at automatically if not set" do it "should set run_at automatically if not set" do
@backend.create(:payload_object => ErrorJob.new ).run_at.should_not be_nil Delayed::Job.create(:payload_object => ErrorJob.new ).run_at.should_not be_nil
end end
it "should not set run_at automatically if already set" do it "should not set run_at automatically if already set" do
later = @backend.db_time_now + 5.minutes later = Delayed::Job.db_time_now + 5.minutes
@backend.create(:payload_object => ErrorJob.new, :run_at => later).run_at.should be_close(later, 1) Delayed::Job.create(:payload_object => ErrorJob.new, :run_at => later).run_at.should be_close(later, 1)
end end
it "should raise ArgumentError when handler doesn't respond_to :perform" do it "should raise ArgumentError when handler doesn't respond_to :perform" do
lambda { @backend.enqueue(Object.new) }.should raise_error(ArgumentError) lambda { Delayed::Job.enqueue(Object.new) }.should raise_error(ArgumentError)
end end
it "should increase count after enqueuing items" do it "should increase count after enqueuing items" do
@backend.enqueue SimpleJob.new Delayed::Job.enqueue SimpleJob.new
@backend.count.should == 1 Delayed::Job.count.should == 1
end end
it "should be able to set priority when enqueuing items" do it "should be able to set priority when enqueuing items" do
@job = @backend.enqueue SimpleJob.new, :priority => 5 @job = Delayed::Job.enqueue SimpleJob.new, :priority => 5
@job.priority.should == 5 @job.priority.should == 5
end end
it "should use the default priority when enqueuing items" do it "should use the default priority when enqueuing items" do
@backend.default_priority = 0 Delayed::Job.default_priority = 0
@job = @backend.enqueue SimpleJob.new @job = Delayed::Job.enqueue SimpleJob.new
@job.priority.should == 0 @job.priority.should == 0
@backend.default_priority = 10 Delayed::Job.default_priority = 10
@job = @backend.enqueue SimpleJob.new @job = Delayed::Job.enqueue SimpleJob.new
@job.priority.should == 10 @job.priority.should == 10
@backend.default_priority = 0 Delayed::Job.default_priority = 0
end end
it "should be able to set run_at when enqueuing items" do it "should be able to set run_at when enqueuing items" do
later = @backend.db_time_now + 5.minutes later = Delayed::Job.db_time_now + 5.minutes
@job = @backend.enqueue SimpleJob.new, :priority => 5, :run_at => later @job = Delayed::Job.enqueue SimpleJob.new, :priority => 5, :run_at => later
@job.run_at.should be_close(later, 1) @job.run_at.should be_close(later, 1)
end end
it "should work with jobs in modules" do it "should work with jobs in modules" do
M::ModuleJob.runs = 0 M::ModuleJob.runs = 0
job = @backend.enqueue M::ModuleJob.new job = Delayed::Job.enqueue M::ModuleJob.new
lambda { job.invoke_job }.should change { M::ModuleJob.runs }.from(0).to(1) lambda { job.invoke_job }.should change { M::ModuleJob.runs }.from(0).to(1)
end end
it "should raise an DeserializationError when the job class is totally unknown" do it "should raise an DeserializationError when the job class is totally unknown" do
job = @backend.new :handler => "--- !ruby/object:JobThatDoesNotExist {}" job = Delayed::Job.new :handler => "--- !ruby/object:JobThatDoesNotExist {}"
lambda { job.payload_object.perform }.should raise_error(Delayed::Backend::DeserializationError) lambda { job.payload_object.perform }.should raise_error(Delayed::Backend::DeserializationError)
end end
it "should try to load the class when it is unknown at the time of the deserialization" do it "should try to load the class when it is unknown at the time of the deserialization" do
job = @backend.new :handler => "--- !ruby/object:JobThatDoesNotExist {}" job = Delayed::Job.new :handler => "--- !ruby/object:JobThatDoesNotExist {}"
lambda { job.payload_object.perform }.should raise_error(Delayed::Backend::DeserializationError) lambda { job.payload_object.perform }.should raise_error(Delayed::Backend::DeserializationError)
end end
it "should try include the namespace when loading unknown objects" do it "should try include the namespace when loading unknown objects" do
job = @backend.new :handler => "--- !ruby/object:Delayed::JobThatDoesNotExist {}" job = Delayed::Job.new :handler => "--- !ruby/object:Delayed::JobThatDoesNotExist {}"
lambda { job.payload_object.perform }.should raise_error(Delayed::Backend::DeserializationError) lambda { job.payload_object.perform }.should raise_error(Delayed::Backend::DeserializationError)
end end
it "should also try to load structs when they are unknown (raises TypeError)" do it "should also try to load structs when they are unknown (raises TypeError)" do
job = @backend.new :handler => "--- !ruby/struct:JobThatDoesNotExist {}" job = Delayed::Job.new :handler => "--- !ruby/struct:JobThatDoesNotExist {}"
lambda { job.payload_object.perform }.should raise_error(Delayed::Backend::DeserializationError) lambda { job.payload_object.perform }.should raise_error(Delayed::Backend::DeserializationError)
end end
it "should try include the namespace when loading unknown structs" do it "should try include the namespace when loading unknown structs" do
job = @backend.new :handler => "--- !ruby/struct:Delayed::JobThatDoesNotExist {}" job = Delayed::Job.new :handler => "--- !ruby/struct:Delayed::JobThatDoesNotExist {}"
lambda { job.payload_object.perform }.should raise_error(Delayed::Backend::DeserializationError) lambda { job.payload_object.perform }.should raise_error(Delayed::Backend::DeserializationError)
end end
@ -81,88 +81,86 @@ shared_examples_for 'a backend' do
it "should not find failed jobs" do it "should not find failed jobs" do
@job = create_job :attempts => 50 @job = create_job :attempts => 50
@job.fail! @job.fail!
@backend.find_available('worker', 5, 1.second).should_not include(@job) Delayed::Job.find_available(5).should_not include(@job)
end end
it "should not find jobs scheduled for the future" do it "should not find jobs scheduled for the future" do
@job = create_job :run_at => (@backend.db_time_now + 1.minute) @job = create_job :run_at => (Delayed::Job.db_time_now + 1.minute)
@backend.find_available('worker', 5, 4.hours).should_not include(@job) Delayed::Job.find_available(5).should_not include(@job)
end end
it "should not find jobs locked by another worker" do it "should not find jobs locked by another worker" do
@job = create_job(:locked_by => 'other_worker', :locked_at => @backend.db_time_now - 1.minute) @job = create_job
@backend.find_available('worker', 5, 4.hours).should_not include(@job) Delayed::Job.get_and_lock_next_available('other_worker').should == @job
Delayed::Job.find_available(5).should_not include(@job)
end end
it "should find open jobs" do it "should find open jobs" do
@job = create_job @job = create_job
@backend.find_available('worker', 5, 4.hours).should include(@job) Delayed::Job.find_available(5).should include(@job)
end end
it "should find expired jobs" do it "should find expired jobs" do
@job = create_job(:locked_by => 'worker', :locked_at => @backend.db_time_now - 2.minutes) @job = create_job
Delayed::Job.get_and_lock_next_available('other_worker').should == @job
@job.update_attribute(:locked_at, Delayed::Job.db_time_now - 2.minutes)
Delayed::Job.unlock_expired_jobs(1.minute) Delayed::Job.unlock_expired_jobs(1.minute)
@backend.find_available('worker', 5, 1.minute).should include(@job) Delayed::Job.find_available(5).should include(@job)
end end
end end
context "when another worker is already performing an task, it" do context "when another worker is already performing an task, it" do
before :each do before :each do
@job = @backend.create :payload_object => SimpleJob.new, :locked_by => 'worker1', :locked_at => @backend.db_time_now - 5.minutes @job = Delayed::Job.create :payload_object => SimpleJob.new
Delayed::Job.get_and_lock_next_available('worker1').should == @job
end end
it "should not allow a second worker to get exclusive access" do it "should not allow a second worker to get exclusive access" do
@job.lock_exclusively!(4.hours, 'worker2').should == false Delayed::Job.get_and_lock_next_available('worker2').should be_nil
end end
it "should allow a second worker to get exclusive access if the timeout has passed" do it "should allow a second worker to get exclusive access if the timeout has passed" do
Delayed::Job.unlock_expired_jobs(1.minute) @job.update_attribute(:locked_at, 5.hours.ago)
@job.lock_exclusively!(1.minute, 'worker2').should == true
end
it "should be able to get access to the task if it was started more then max_age ago" do
@job.locked_at = 5.hours.ago
@job.save
Delayed::Job.unlock_expired_jobs(4.hours) Delayed::Job.unlock_expired_jobs(4.hours)
@job.lock_exclusively! 4.hours, 'worker2' Delayed::Job.get_and_lock_next_available('worker2').should == @job
@job.reload @job.reload
@job.locked_by.should == 'worker2' @job.locked_by.should == 'worker2'
@job.locked_at.should > 1.minute.ago @job.locked_at.should > 1.minute.ago
end end
it "should not be found by another worker" do it "should not be found by another worker" do
@backend.find_available('worker2', 1, 6.minutes).length.should == 0 Delayed::Job.find_available(1).length.should == 0
end end
it "should be found by another worker if the time has expired" do it "should be found by another worker if the time has expired" do
Delayed::Job.unlock_expired_jobs(4.minutes) @job.update_attribute(:locked_at, 5.hours.ago)
@backend.find_available('worker2', 1, 4.minutes).length.should == 1 Delayed::Job.unlock_expired_jobs(4.hours)
Delayed::Job.find_available(5).length.should == 1
end end
end end
context "when another worker has worked on a task since the job was found to be available, it" do context "when another worker has worked on a task since the job was found to be available, it" do
before :each do before :each do
@job = @backend.create :payload_object => SimpleJob.new @job = Delayed::Job.create :payload_object => SimpleJob.new
@job_copy_for_worker_2 = @backend.find(@job.id) @job_copy_for_worker_2 = Delayed::Job.find(@job.id)
end end
it "should not allow a second worker to get exclusive access if already successfully processed by worker1" do it "should not allow a second worker to get exclusive access if already successfully processed by worker1" do
@job.destroy @job.destroy
@job_copy_for_worker_2.lock_exclusively!(4.hours, 'worker2').should == false @job_copy_for_worker_2.lock_exclusively!('worker2').should == false
end end
it "should not allow a second worker to get exclusive access if failed to be processed by worker1 and run_at time is now in future (due to backing off behaviour)" do it "should not allow a second worker to get exclusive access if failed to be processed by worker1 and run_at time is now in future (due to backing off behaviour)" do
@job.update_attributes(:attempts => 1, :run_at => 1.day.from_now) @job.update_attributes(:attempts => 1, :run_at => 1.day.from_now)
@job_copy_for_worker_2.lock_exclusively!(4.hours, 'worker2').should == false @job_copy_for_worker_2.lock_exclusively!('worker2').should == false
end end
end end
context "#name" do context "#name" do
it "should be the class name of the job that was enqueued" do it "should be the class name of the job that was enqueued" do
@backend.create(:payload_object => ErrorJob.new ).name.should == 'ErrorJob' Delayed::Job.create(:payload_object => ErrorJob.new ).name.should == 'ErrorJob'
end end
it "should be the method that will be called if its a performable method object" do it "should be the method that will be called if its a performable method object" do
@ -179,7 +177,7 @@ shared_examples_for 'a backend' do
context "worker prioritization" do context "worker prioritization" do
it "should fetch jobs ordered by priority" do it "should fetch jobs ordered by priority" do
10.times { create_job :priority => rand(10) } 10.times { create_job :priority => rand(10) }
jobs = @backend.find_available('worker', 10, 10) jobs = Delayed::Job.find_available(10)
jobs.size.should == 10 jobs.size.should == 10
jobs.each_cons(2) do |a, b| jobs.each_cons(2) do |a, b|
a.priority.should <= b.priority a.priority.should <= b.priority
@ -189,23 +187,23 @@ shared_examples_for 'a backend' do
context "clear_locks!" do context "clear_locks!" do
before do before do
@job = create_job(:locked_by => 'worker', :locked_at => @backend.db_time_now) @job = create_job(:locked_by => 'worker', :locked_at => Delayed::Job.db_time_now)
end end
it "should clear locks for the given worker" do it "should clear locks for the given worker" do
@backend.clear_locks!('worker') Delayed::Job.clear_locks!('worker')
@backend.find_available('worker2', 5, 1.minute).should include(@job) Delayed::Job.find_available(5).should include(@job)
end end
it "should not clear locks for other workers" do it "should not clear locks for other workers" do
@backend.clear_locks!('worker1') Delayed::Job.clear_locks!('worker1')
@backend.find_available('worker1', 5, 1.minute).should_not include(@job) Delayed::Job.find_available(5).should_not include(@job)
end end
end end
context "unlock" do context "unlock" do
before do before do
@job = create_job(:locked_by => 'worker', :locked_at => @backend.db_time_now) @job = create_job(:locked_by => 'worker', :locked_at => Delayed::Job.db_time_now)
end end
it "should clear locks" do it "should clear locks" do
@ -219,85 +217,85 @@ shared_examples_for 'a backend' do
it "should run strand jobs in strict order" do it "should run strand jobs in strict order" do
job1 = create_job(:strand => 'myjobs') job1 = create_job(:strand => 'myjobs')
job2 = create_job(:strand => 'myjobs') job2 = create_job(:strand => 'myjobs')
@backend.get_and_lock_next_available('w1', 60).should == job1 Delayed::Job.get_and_lock_next_available('w1').should == job1
@backend.get_and_lock_next_available('w2', 60).should == nil Delayed::Job.get_and_lock_next_available('w2').should == nil
job1.destroy job1.destroy
# update time since the failed lock pushed it forward # update time since the failed lock pushed it forward
job2.update_attribute(:run_at, 1.minute.ago) job2.update_attribute(:run_at, 1.minute.ago)
@backend.get_and_lock_next_available('w3', 60).should == job2 Delayed::Job.get_and_lock_next_available('w3').should == job2
@backend.get_and_lock_next_available('w4', 60).should == nil Delayed::Job.get_and_lock_next_available('w4').should == nil
end end
it "should fail to lock if an earlier job gets locked" do it "should fail to lock if an earlier job gets locked" do
job1 = create_job(:strand => 'myjobs') job1 = create_job(:strand => 'myjobs')
job2 = create_job(:strand => 'myjobs') job2 = create_job(:strand => 'myjobs')
@backend.find_available('w1', 2, 60).should == [job1] Delayed::Job.find_available(2).should == [job1]
@backend.find_available('w2', 2, 60).should == [job1] Delayed::Job.find_available(2).should == [job1]
# job1 gets locked by w1 # job1 gets locked by w1
job1.lock_exclusively!(60, 'w1').should == true job1.lock_exclusively!('w1').should == true
# w2 tries to lock job1, fails # w2 tries to lock job1, fails
job1.lock_exclusively!(60, 'w2').should == false job1.lock_exclusively!('w2').should == false
# normally w2 would now be able to lock job2, but strands prevent it # normally w2 would now be able to lock job2, but strands prevent it
@backend.get_and_lock_next_available('w2', 60).should be_nil Delayed::Job.get_and_lock_next_available('w2').should be_nil
# now job1 is done # now job1 is done
job1.destroy job1.destroy
# update time since the failed lock pushed it forward # update time since the failed lock pushed it forward
job2.update_attribute(:run_at, 1.minute.ago) job2.update_attribute(:run_at, 1.minute.ago)
job2.lock_exclusively!(60, 'w2').should == true job2.lock_exclusively!('w2').should == true
end end
it "should keep strand jobs in order as they are rescheduled" do it "should keep strand jobs in order as they are rescheduled" do
job1 = create_job(:strand => 'myjobs') job1 = create_job(:strand => 'myjobs')
job2 = create_job(:strand => 'myjobs') job2 = create_job(:strand => 'myjobs')
job3 = create_job(:strand => 'myjobs') job3 = create_job(:strand => 'myjobs')
@backend.get_and_lock_next_available('w1', 60).should == job1 Delayed::Job.get_and_lock_next_available('w1').should == job1
@backend.find_available('w2', 1, 60).should == [] Delayed::Job.find_available(1).should == []
job1.destroy job1.destroy
# move job2's time forward # move job2's time forward
job2.update_attribute(:run_at, 1.second.ago) job2.update_attribute(:run_at, 1.second.ago)
job3.update_attribute(:run_at, 5.seconds.ago) job3.update_attribute(:run_at, 5.seconds.ago)
# we should still get job2, not job3 # we should still get job2, not job3
@backend.get_and_lock_next_available('w1', 60).should == job2 Delayed::Job.get_and_lock_next_available('w1').should == job2
end end
it "should allow to run the next job if a failed job is present" do it "should allow to run the next job if a failed job is present" do
job1 = create_job(:strand => 'myjobs') job1 = create_job(:strand => 'myjobs')
job2 = create_job(:strand => 'myjobs') job2 = create_job(:strand => 'myjobs')
job1.fail! job1.fail!
@backend.find_available('w1', 2, 60).should == [job2] Delayed::Job.find_available(2).should == [job2]
job2.lock_exclusively!(60, 'w1').should == true job2.lock_exclusively!('w1').should == true
end end
it "should not interfere with jobs with no strand" do it "should not interfere with jobs with no strand" do
job1 = create_job(:strand => nil) job1 = create_job(:strand => nil)
job2 = create_job(:strand => 'myjobs') job2 = create_job(:strand => 'myjobs')
@backend.get_and_lock_next_available('w1', 60).should == job1 Delayed::Job.get_and_lock_next_available('w1').should == job1
@backend.get_and_lock_next_available('w2', 60).should == job2 Delayed::Job.get_and_lock_next_available('w2').should == job2
@backend.get_and_lock_next_available('w3', 60).should == nil Delayed::Job.get_and_lock_next_available('w3').should == nil
end end
it "should not interfere with jobs in other strands" do it "should not interfere with jobs in other strands" do
job1 = create_job(:strand => 'strand1') job1 = create_job(:strand => 'strand1')
job2 = create_job(:strand => 'strand2') job2 = create_job(:strand => 'strand2')
@backend.get_and_lock_next_available('w1', 60).should == job1 Delayed::Job.get_and_lock_next_available('w1').should == job1
@backend.get_and_lock_next_available('w2', 60).should == job2 Delayed::Job.get_and_lock_next_available('w2').should == job2
@backend.get_and_lock_next_available('w3', 60).should == nil Delayed::Job.get_and_lock_next_available('w3').should == nil
end end
context 'singleton' do context 'singleton' do
it "should create if there's no jobs on the strand" do it "should create if there's no jobs on the strand" do
@job = create_job(:singleton => 'myjobs') @job = create_job(:singleton => 'myjobs')
@job.should be_present @job.should be_present
@backend.get_and_lock_next_available('w1', 60).should == @job Delayed::Job.get_and_lock_next_available('w1').should == @job
end end
it "should create if there's another job on the strand, but it's running" do it "should create if there's another job on the strand, but it's running" do
@job = create_job(:singleton => 'myjobs') @job = create_job(:singleton => 'myjobs')
@job.should be_present @job.should be_present
@backend.get_and_lock_next_available('w1', 60).should == @job Delayed::Job.get_and_lock_next_available('w1').should == @job
@job2 = create_job(:singleton => 'myjobs') @job2 = create_job(:singleton => 'myjobs')
@job.should be_present @job.should be_present
@ -315,7 +313,7 @@ shared_examples_for 'a backend' do
it "should not create if there's a job running and one waiting on the strand" do it "should not create if there's a job running and one waiting on the strand" do
@job = create_job(:singleton => 'myjobs') @job = create_job(:singleton => 'myjobs')
@job.should be_present @job.should be_present
@backend.get_and_lock_next_available('w1', 60).should == @job Delayed::Job.get_and_lock_next_available('w1').should == @job
@job2 = create_job(:singleton => 'myjobs') @job2 = create_job(:singleton => 'myjobs')
@job2.should be_present @job2.should be_present
@ -346,10 +344,10 @@ shared_examples_for 'a backend' do
it "should hold/unhold jobs" do it "should hold/unhold jobs" do
job1 = create_job() job1 = create_job()
job1.hold! job1.hold!
@backend.get_and_lock_next_available('w1', 60).should be_nil Delayed::Job.get_and_lock_next_available('w1').should be_nil
job1.unhold! job1.unhold!
@backend.get_and_lock_next_available('w1', 60).should == job1 Delayed::Job.get_and_lock_next_available('w1').should == job1
end end
it "should hold a scope of jobs" do it "should hold a scope of jobs" do
@ -378,66 +376,66 @@ shared_examples_for 'a backend' do
before(:each) do before(:each) do
Delayed::Periodic.scheduled = {} Delayed::Periodic.scheduled = {}
Delayed::Periodic.cron('my SimpleJob', '*/5 * * * * *') do Delayed::Periodic.cron('my SimpleJob', '*/5 * * * * *') do
@backend.enqueue(SimpleJob.new) Delayed::Job.enqueue(SimpleJob.new)
end end
end end
it "should schedule jobs if they aren't scheduled yet" do it "should schedule jobs if they aren't scheduled yet" do
@backend.count.should == 0 Delayed::Job.count.should == 0
audit_started = @backend.db_time_now audit_started = Delayed::Job.db_time_now
Delayed::Periodic.perform_audit! Delayed::Periodic.perform_audit!
@backend.count.should == 1 Delayed::Job.count.should == 1
job = @backend.first job = Delayed::Job.first
job.tag.should == 'periodic: my SimpleJob' job.tag.should == 'periodic: my SimpleJob'
job.payload_object.should == Delayed::Periodic.scheduled['my SimpleJob'] job.payload_object.should == Delayed::Periodic.scheduled['my SimpleJob']
job.run_at.should >= audit_started job.run_at.should >= audit_started
job.run_at.should <= @backend.db_time_now + 6.minutes job.run_at.should <= Delayed::Job.db_time_now + 6.minutes
job.strand.should == job.tag job.strand.should == job.tag
end end
it "should schedule jobs if there are only failed jobs on the queue" do it "should schedule jobs if there are only failed jobs on the queue" do
@backend.count.should == 0 Delayed::Job.count.should == 0
expect { Delayed::Periodic.perform_audit! }.to change(@backend, :count).by(1) expect { Delayed::Periodic.perform_audit! }.to change(Delayed::Job, :count).by(1)
@backend.count.should == 1 Delayed::Job.count.should == 1
job = @backend.first job = Delayed::Job.first
job.fail! job.fail!
expect { Delayed::Periodic.perform_audit! }.to change(@backend, :count).by(1) expect { Delayed::Periodic.perform_audit! }.to change(Delayed::Job, :count).by(1)
end end
it "should not schedule jobs that are already scheduled" do it "should not schedule jobs that are already scheduled" do
@backend.count.should == 0 Delayed::Job.count.should == 0
Delayed::Periodic.perform_audit! Delayed::Periodic.perform_audit!
@backend.count.should == 1 Delayed::Job.count.should == 1
job = @backend.first job = Delayed::Job.first
Delayed::Periodic.perform_audit! Delayed::Periodic.perform_audit!
@backend.count.should == 1 Delayed::Job.count.should == 1
job.should == @backend.first job.should == Delayed::Job.first
end end
it "should aduit on the auditor strand" do it "should aduit on the auditor strand" do
Delayed::Periodic.audit_queue Delayed::Periodic.audit_queue
@backend.count.should == 1 Delayed::Job.count.should == 1
@backend.first.strand.should == Delayed::Periodic::STRAND Delayed::Job.first.strand.should == Delayed::Periodic::STRAND
end end
it "should only schedule an audit if none is scheduled" do it "should only schedule an audit if none is scheduled" do
Delayed::Periodic.audit_queue Delayed::Periodic.audit_queue
@backend.count.should == 1 Delayed::Job.count.should == 1
Delayed::Periodic.audit_queue Delayed::Periodic.audit_queue
@backend.count.should == 1 Delayed::Job.count.should == 1
end end
it "should schedule the next job run after performing" do it "should schedule the next job run after performing" do
Delayed::Periodic.perform_audit! Delayed::Periodic.perform_audit!
job = @backend.first job = Delayed::Job.first
run_job(job) run_job(job)
job.destroy job.destroy
@backend.count.should == 2 Delayed::Job.count.should == 2
job = @backend.first(:order => 'run_at asc') job = Delayed::Job.first(:order => 'run_at asc')
job.tag.should == 'SimpleJob#perform' job.tag.should == 'SimpleJob#perform'
next_scheduled = @backend.last(:order => 'run_at asc') next_scheduled = Delayed::Job.last(:order => 'run_at asc')
next_scheduled.tag.should == 'periodic: my SimpleJob' next_scheduled.tag.should == 'periodic: my SimpleJob'
next_scheduled.payload_object.should be_is_a(Delayed::Periodic) next_scheduled.payload_object.should be_is_a(Delayed::Periodic)
next_scheduled.run_at.utc.to_i.should >= Time.now.utc.to_i next_scheduled.run_at.utc.to_i.should >= Time.now.utc.to_i
@ -464,7 +462,7 @@ shared_examples_for 'a backend' do
Setting.set_config('periodic_jobs', { 'my ChangedJob' => '*/10 * * * * *' }) Setting.set_config('periodic_jobs', { 'my ChangedJob' => '*/10 * * * * *' })
Delayed::Periodic.scheduled = {} Delayed::Periodic.scheduled = {}
Delayed::Periodic.cron('my ChangedJob', '*/5 * * * * *') do Delayed::Periodic.cron('my ChangedJob', '*/5 * * * * *') do
@backend.enqueue(SimpleJob.new) Delayed::Job.enqueue(SimpleJob.new)
end end
Delayed::Periodic.scheduled['my ChangedJob'].cron.original.should == '*/10 * * * * *' Delayed::Periodic.scheduled['my ChangedJob'].cron.original.should == '*/10 * * * * *'
Delayed::Periodic.audit_overrides! Delayed::Periodic.audit_overrides!
@ -474,7 +472,7 @@ shared_examples_for 'a backend' do
Setting.set_config('periodic_jobs', { 'my ChangedJob' => '*/10 * * * * * *' }) # extra asterisk Setting.set_config('periodic_jobs', { 'my ChangedJob' => '*/10 * * * * * *' }) # extra asterisk
Delayed::Periodic.scheduled = {} Delayed::Periodic.scheduled = {}
expect { Delayed::Periodic.cron('my ChangedJob', '*/5 * * * * *') do expect { Delayed::Periodic.cron('my ChangedJob', '*/5 * * * * *') do
@backend.enqueue(SimpleJob.new) Delayed::Job.enqueue(SimpleJob.new)
end }.to raise_error end }.to raise_error
expect { Delayed::Periodic.audit_overrides! }.to raise_error expect { Delayed::Periodic.audit_overrides! }.to raise_error
@ -493,4 +491,14 @@ shared_examples_for 'a backend' do
job.invoke_job job.invoke_job
Delayed::Job.in_delayed_job?.should == false Delayed::Job.in_delayed_job?.should == false
end end
it "should fail on job creation if an unsaved AR object is used" do
story = Story.new :text => "Once upon..."
lambda { story.send_later(:text) }.should raise_error
reader = StoryReader.new
lambda { reader.send_later(:read, story) }.should raise_error
lambda { [story, 1, story, false].send_later(:first) }.should raise_error
end
end end

View File

@ -0,0 +1,15 @@
require File.expand_path('../shared_backend_spec', __FILE__)
require File.expand_path('../delayed_batch_spec', __FILE__)
require File.expand_path('../delayed_method_spec', __FILE__)
require File.expand_path('../performable_method_spec', __FILE__)
require File.expand_path('../stats_spec', __FILE__)
require File.expand_path('../worker_spec', __FILE__)
shared_examples_for 'a delayed_jobs implementation' do
it_should_behave_like 'a backend'
it_should_behave_like 'Delayed::Batch'
it_should_behave_like 'random ruby objects'
it_should_behave_like 'Delayed::PerformableMethod'
it_should_behave_like 'Delayed::Stats'
it_should_behave_like 'Delayed::Worker'
end

View File

@ -27,4 +27,4 @@ module MyReverser
end end
require File.expand_path('../sample_jobs', __FILE__) require File.expand_path('../sample_jobs', __FILE__)
require File.expand_path('../shared_backend_spec', __FILE__) require File.expand_path('../shared_jobs_spec', __FILE__)

View File

@ -1,14 +1,12 @@
require File.expand_path("../spec_helper", __FILE__)
if Canvas.redis_enabled? if Canvas.redis_enabled?
describe Delayed::Stats do shared_examples_for 'Delayed::Stats' do
before do before do
Setting.set('delayed_jobs_store_stats', 'redis') Setting.set('delayed_jobs_store_stats', 'redis')
end end
it "should store stats for jobs" do it "should store stats for jobs" do
job = "ohai".send_later(:reverse) job = "ohai".send_later(:reverse)
job.lock_exclusively!(60, 'stub worker').should be_true job.lock_exclusively!('stub worker').should be_true
worker = mock('Delayed::Worker') worker = mock('Delayed::Worker')
worker.stubs(:name).returns("stub worker") worker.stubs(:name).returns("stub worker")
Delayed::Stats.job_complete(job, worker) Delayed::Stats.job_complete(job, worker)
@ -18,7 +16,7 @@ describe Delayed::Stats do
it "should completely clean up after stats" do it "should completely clean up after stats" do
job = "ohai".send_later(:reverse) job = "ohai".send_later(:reverse)
job.lock_exclusively!(60, 'stub worker').should be_true job.lock_exclusively!('stub worker').should be_true
worker = mock('Delayed::Worker') worker = mock('Delayed::Worker')
worker.stubs(:name).returns("stub worker") worker.stubs(:name).returns("stub worker")

View File

@ -1,6 +1,4 @@
require File.expand_path("../spec_helper", __FILE__) shared_examples_for 'Delayed::Worker' do
describe Delayed::Worker do
def job_create(opts = {}) def job_create(opts = {})
Delayed::Job.create({:payload_object => SimpleJob.new, :queue => Delayed::Worker.queue}.merge(opts)) Delayed::Job.create({:payload_object => SimpleJob.new, :queue => Delayed::Worker.queue}.merge(opts))
end end
@ -177,7 +175,7 @@ describe Delayed::Worker do
it "should record last_error when destroy_failed_jobs = false, max_attempts = 1" do it "should record last_error when destroy_failed_jobs = false, max_attempts = 1" do
Delayed::Worker.on_max_failures = proc { false } Delayed::Worker.on_max_failures = proc { false }
@job.update_attribute(:max_attempts, 1) @job.update_attribute(:max_attempts, 1)
@job.lock_exclusively!(Delayed::Worker.max_run_time, @worker.name).should == true @job.lock_exclusively!(@worker.name).should == true
@worker.perform(@job) @worker.perform(@job)
old_id = @job.id old_id = @job.id
@job = Delayed::Job::Failed.first @job = Delayed::Job::Failed.first
@ -191,9 +189,7 @@ describe Delayed::Worker do
# job stays locked after failing, for record keeping of time/worker # job stays locked after failing, for record keeping of time/worker
@job.should be_locked @job.should be_locked
all_jobs = Delayed::Job.all_available(@worker.name, all_jobs = Delayed::Job.all_available(@job.queue)
Delayed::Worker.max_run_time,
@job.queue)
all_jobs.find_by_id(@job.id).should be_nil all_jobs.find_by_id(@job.id).should be_nil
end end