diff --git a/README.markdown b/README.markdown index 188bb87e..66a0a978 100644 --- a/README.markdown +++ b/README.markdown @@ -128,6 +128,12 @@ down when a particular job is supposed to be queue, they will simply "catch up" once they are started again. Jobs are guaranteed to run (provided they make it into the delayed queue) after their given queue_at time has passed. +Similar to `before_enqueue` and `after_enqueue` hooks provided in Resque +(>= 1.19.1), your jobs can specify one or more `before_schedule` and +`after_schedule` hooks, to be run before or after scheduling. If *any* of your +`before_schedule` hooks returns `false`, the job will *not* be scheduled and +your `after_schedule` hooks will *not* be run. + One other thing to note is that insertion into the delayed queue is O(log(n)) since the jobs are stored in a redis sorted set (zset). I can't imagine this being an issue for someone since redis is stupidly fast even at log(n), but full diff --git a/lib/resque_scheduler.rb b/lib/resque_scheduler.rb index 276eeb7f..093cebc0 100644 --- a/lib/resque_scheduler.rb +++ b/lib/resque_scheduler.rb @@ -102,14 +102,23 @@ def remove_schedule(name) # sit in the schedule list. def enqueue_at(timestamp, klass, *args) validate_job!(klass) - delayed_push(timestamp, job_to_hash(klass, args)) + enqueue_at_with_queue( queue_from_class(klass), timestamp, klass, *args) end # Identical to +enqueue_at+, except you can also specify # a queue in which the job will be placed after the # timestamp has passed. def enqueue_at_with_queue(queue, timestamp, klass, *args) + before_hooks = before_schedule_hooks(klass).collect do |hook| + klass.send(hook,*args) + end + return false if before_hooks.any? { |result| result == false } + delayed_push(timestamp, job_to_hash_with_queue(queue, klass, args)) + + after_schedule_hooks(klass).collect do |hook| + klass.send(hook,*args) + end end # Identical to enqueue_at but takes number_of_seconds_from_now @@ -240,6 +249,14 @@ def validate_job!(klass) end end + def before_schedule_hooks(klass) + klass.methods.grep(/^before_schedule/).sort + end + + def after_schedule_hooks(klass) + klass.methods.grep(/^after_schedule/).sort + end + end Resque.extend ResqueScheduler diff --git a/test/scheduler_hooks_test.rb b/test/scheduler_hooks_test.rb new file mode 100644 index 00000000..83384cba --- /dev/null +++ b/test/scheduler_hooks_test.rb @@ -0,0 +1,52 @@ +require File.dirname(__FILE__) + '/test_helper' + +context "scheduling jobs with hooks" do + class JobThatCannotBeScheduledWithoutArguments < Resque::Job + @queue = :job_that_cannot_be_scheduled_without_arguments + def self.perform(*x);end + def self.before_schedule_return_nil_if_arguments_not_supplied(*args) + counters[:before_schedule] += 1 + return false if args.empty? + end + + def self.after_schedule_do_something(*args) + counters[:after_schedule] += 1 + end + + class << self + def counters + @counters ||= Hash.new{|h,k| h[k]=0} + end + def clean + counters.clear + self + end + end + end + + setup do + Resque::Scheduler.dynamic = false + Resque.redis.del(:schedules) + Resque.redis.del(:schedules_changed) + Resque::Scheduler.mute = true + Resque::Scheduler.clear_schedule! + Resque::Scheduler.send(:class_variable_set, :@@scheduled_jobs, {}) + end + + test "before_schedule hook that does not return false should not block" do + enqueue_time = Time.now + 12 + Resque.enqueue_at(enqueue_time, JobThatCannotBeScheduledWithoutArguments.clean, :foo) + assert_equal(1, Resque.delayed_timestamp_size(enqueue_time.to_i), "delayed queue should have one entry now") + assert_equal(1, JobThatCannotBeScheduledWithoutArguments.counters[:before_schedule], 'before_schedule was not run') + assert_equal(1, JobThatCannotBeScheduledWithoutArguments.counters[:after_schedule], 'after_schedule was not run') + end + + test "before_schedule hook that returns false should block" do + enqueue_time = Time.now + 60 + assert_equal(0, JobThatCannotBeScheduledWithoutArguments.clean.counters[:before_schedule], 'before_schedule should be zero') + Resque.enqueue_at(enqueue_time, JobThatCannotBeScheduledWithoutArguments.clean) + assert_equal(0, Resque.delayed_timestamp_size(enqueue_time.to_i), "job should not have been put in queue") + assert_equal(1, JobThatCannotBeScheduledWithoutArguments.counters[:before_schedule], 'before_schedule was not run') + assert_equal(0, JobThatCannotBeScheduledWithoutArguments.counters[:after_schedule], 'after_schedule was run') + end +end