diff --git a/README.md b/README.md index ed210700..f4d6e7b8 100644 --- a/README.md +++ b/README.md @@ -59,6 +59,14 @@ every 3.hours do # 1.minute 1.day 1.week 1.month 1.year is also supported command "/usr/bin/my_great_command" end +every 3.hours, sequential: true do + # Jobs in this block that don't set their own sequence will default to run in the same sequence (not in parallel) + runner "MyModel.some_process", halt_sequence_on_failure: true # If this job fails, subsequent jobs will not run (defaults to false) + rake "my:rake:task" + command "/usr/bin/my_great_command", sequence: nil # This job runs in parallel with other jobs that are not part of a sequence + runner "MyModel.task_to_run_at_four_thirty_in_the_morning", sequence: 'other' # This job runs sequentially with other jobs in the sequence called 'other' +end + every 1.day, at: '4:30 am' do runner "MyModel.task_to_run_at_four_thirty_in_the_morning" end diff --git a/lib/whenever.rb b/lib/whenever.rb index d2ef6dd7..4206f42d 100644 --- a/lib/whenever.rb +++ b/lib/whenever.rb @@ -1,6 +1,7 @@ require 'whenever/numeric' require 'whenever/numeric_seconds' require 'whenever/job_list' +require 'whenever/job_sequence' require 'whenever/job' require 'whenever/command_line' require 'whenever/cron' diff --git a/lib/whenever/job.rb b/lib/whenever/job.rb index 2dad8329..a56063cd 100644 --- a/lib/whenever/job.rb +++ b/lib/whenever/job.rb @@ -2,7 +2,7 @@ module Whenever class Job - attr_reader :at, :roles, :mailto + attr_reader :at, :roles, :mailto, :sequence, :halt_sequence_on_failure def initialize(options = {}) @options = options @@ -10,6 +10,8 @@ def initialize(options = {}) @template = options.delete(:template) @mailto = options.fetch(:mailto, :default_mailto) @job_template = options.delete(:job_template) || ":job" + @sequence = options.delete(:sequence) + @halt_sequence_on_failure = options.delete(:halt_sequence_on_failure) @roles = Array(options.delete(:roles)) @options[:output] = options.has_key?(:output) ? Whenever::Output::Redirection.new(options[:output]).to_s : '' @options[:environment_variable] ||= "RAILS_ENV" diff --git a/lib/whenever/job_list.rb b/lib/whenever/job_list.rb index 7df39c68..25e497f1 100644 --- a/lib/whenever/job_list.rb +++ b/lib/whenever/job_list.rb @@ -48,6 +48,12 @@ def env(variable, value) def every(frequency, options = {}) @current_time_scope = frequency @options = options + + if @options[:sequential] + @default_sequence_id ||= 0 + @options[:sequence] ||= "default_sequence_#{@default_sequence_id += 1}" + end + yield end @@ -66,7 +72,7 @@ def job_type(name, template) @jobs[options.fetch(:mailto)] ||= {} @jobs[options.fetch(:mailto)][@current_time_scope] ||= [] - @jobs[options.fetch(:mailto)][@current_time_scope] << Whenever::Job.new(@options.merge(@set_variables).merge(options)) + @jobs[options.fetch(:mailto)][@current_time_scope] << Whenever::Job.new(@set_variables.merge(@options).merge(options)) end end end @@ -138,10 +144,17 @@ def combine(entries) def cron_jobs_of_time(time, jobs) shortcut_jobs, regular_jobs = [], [] + filtered_jobs = jobs.select do |job| + roles.empty? || roles.any? { |r| job.has_role?(r) } + end + + grouped_jobs = filtered_jobs.group_by(&:sequence) + grouped_jobs.each do |sequence, jobs| + grouped_jobs[sequence] = JobSequence.new(jobs) if sequence + end + jobs = grouped_jobs.values.flatten + jobs.each do |job| - next unless roles.empty? || roles.any? do |r| - job.has_role?(r) - end Whenever::Output::Cron.output(time, job, :chronic_options => @chronic_options) do |cron| cron << "\n\n" diff --git a/lib/whenever/job_sequence.rb b/lib/whenever/job_sequence.rb new file mode 100644 index 00000000..b63ddad0 --- /dev/null +++ b/lib/whenever/job_sequence.rb @@ -0,0 +1,35 @@ +require 'shellwords' + +module Whenever + class JobSequence + attr_reader :at, :roles, :mailto + + def initialize(jobs, options = {}) + validate!(jobs) + + @jobs = jobs + @options = options + @at = options.fetch(:at, primary_job.at) + @mailto = options.fetch(:mailto, primary_job.mailto || :default_mailto) + @roles = Array(options.delete(:roles), *primary_job.roles) + end + + def output + @jobs.map { |job| [job.output, job.halt_sequence_on_failure ? ' && ' : ' ; '] }.flatten[0..-2].join + end + + def has_role?(role) + roles.empty? || roles.include?(role) + end + + private + + def primary_job + @jobs.first + end + + def validate!(jobs) + raise ArgumentError, "Jobs in a sequence don't support different `at` values" if jobs.map(&:at).uniq.count > 1 + end + end +end diff --git a/test/functional/output_defined_job_test.rb b/test/functional/output_defined_job_test.rb index 9f163f66..6fcacf4b 100644 --- a/test/functional/output_defined_job_test.rb +++ b/test/functional/output_defined_job_test.rb @@ -55,6 +55,34 @@ class OutputDefinedJobTest < Whenever::TestCase assert_match(/^.+ .+ .+ .+ before during after local$/, output) end + test "defined job with a :task and an option where the option is set globally and on the group" do + output = Whenever.cron \ + <<-file + set :job_template, nil + job_type :some_job, "before :task after :option1" + set :option1, 'global' + every 2.hours, :option1 => 'group' do + some_job "during" + end + file + + assert_match(/^.+ .+ .+ .+ before during after group$/, output) + end + + test "defined job with a :task and an option where the option is set globally, on the group, and locally" do + output = Whenever.cron \ + <<-file + set :job_template, nil + job_type :some_job, "before :task after :option1" + set :option1, 'global' + every 2.hours, :option1 => 'group' do + some_job "during", :option1 => 'local' + end + file + + assert_match(/^.+ .+ .+ .+ before during after local$/, output) + end + test "defined job with a :task and an option that is not set" do output = Whenever.cron \ <<-file diff --git a/test/functional/output_jobs_with_sequence_test.rb b/test/functional/output_jobs_with_sequence_test.rb new file mode 100644 index 00000000..15d693b8 --- /dev/null +++ b/test/functional/output_jobs_with_sequence_test.rb @@ -0,0 +1,151 @@ +require 'test_helper' + +class OutputJobsWithSequenceTest < Whenever::TestCase + test "defined jobs with a sequence argument specified per-job" do + output = Whenever.cron \ + <<-file + every 2.hours do + command "blahblah", sequence: 'backups' + command "foofoo", sequence: 'backups' + command "barbar" + end + file + + output_without_empty_line = lines_without_empty_line(output.lines) + assert_equal two_hours + " /bin/bash -l -c 'blahblah' ; /bin/bash -l -c 'foofoo'", output_without_empty_line.shift + assert_equal two_hours + " /bin/bash -l -c 'barbar'", output_without_empty_line.shift + end + + test "defined jobs with a sequence argument specified on the group" do + output = Whenever.cron \ + <<-file + every 2.hours, sequence: 'backups' do + command "blahblah" + command "foofoo" + end + file + + output_without_empty_line = lines_without_empty_line(output.lines) + assert_equal two_hours + " /bin/bash -l -c 'blahblah' ; /bin/bash -l -c 'foofoo'", output_without_empty_line.shift + end + + test "defined jobs with a sequence argument and a job that halts the sequence on failure" do + output = Whenever.cron \ + <<-file + every 2.hours, sequence: 'backups' do + command "blahblah", halt_sequence_on_failure: true + command "foofoo" + end + file + + output_without_empty_line = lines_without_empty_line(output.lines) + assert_equal two_hours + " /bin/bash -l -c 'blahblah' && /bin/bash -l -c 'foofoo'", output_without_empty_line.shift + end + + test "defined jobs with a sequences specified on the group and jobs" do + output = Whenever.cron \ + <<-file + every 2.hours, sequence: 'backups' do + command "blahblah" + command "barbar", sequence: nil + command "foofoo" + command "bazbaz", sequence: 'bees' + command "buzzbuzz", sequence: 'bees' + end + file + + output_without_empty_line = lines_without_empty_line(output.lines) + assert_equal two_hours + " /bin/bash -l -c 'blahblah' ; /bin/bash -l -c 'foofoo'", output_without_empty_line.shift + assert_equal two_hours + " /bin/bash -l -c 'barbar'", output_without_empty_line.shift + assert_equal two_hours + " /bin/bash -l -c 'bazbaz' ; /bin/bash -l -c 'buzzbuzz'", output_without_empty_line.shift + end + + test "defined jobs with a multiple groups with sequences specified on the group and jobs" do + output = Whenever.cron \ + <<-file + every 2.hours, sequence: 'backups' do + command "blahblah" + command "barbar", sequence: nil + command "bazbaz", sequence: 'bees' + end + + every 2.hours, sequence: 'backups' do + command "foofoo" + command "buzzbuzz", sequence: 'bees' + end + file + + output_without_empty_line = lines_without_empty_line(output.lines) + assert_equal two_hours + " /bin/bash -l -c 'blahblah' ; /bin/bash -l -c 'foofoo'", output_without_empty_line.shift + assert_equal two_hours + " /bin/bash -l -c 'barbar'", output_without_empty_line.shift + assert_equal two_hours + " /bin/bash -l -c 'bazbaz' ; /bin/bash -l -c 'buzzbuzz'", output_without_empty_line.shift + end + + test "defined jobs with a multiple groups with sequences specified on the group and jobs" do + output = Whenever.cron \ + <<-file + every 2.hours, sequence: 'backups' do + command "blahblah" + command "barbar", sequence: nil + command "bazbaz", sequence: 'bees' + end + + every 3.hours, sequence: 'backups' do + command "foofoo" + command "buzzbuzz", sequence: 'bees' + end + file + + output_without_empty_line = lines_without_empty_line(output.lines) + assert_equal two_hours + " /bin/bash -l -c 'blahblah'", output_without_empty_line.shift + assert_equal two_hours + " /bin/bash -l -c 'barbar'", output_without_empty_line.shift + assert_equal two_hours + " /bin/bash -l -c 'bazbaz'", output_without_empty_line.shift + assert_equal three_hours + " /bin/bash -l -c 'foofoo'", output_without_empty_line.shift + assert_equal three_hours + " /bin/bash -l -c 'buzzbuzz'", output_without_empty_line.shift + end + + test "defined jobs with a multiple groups with sequences specified on the group and jobs" do + assert_raises ArgumentError do + Whenever.cron \ + <<-file + every 2.hours, sequence: 'backups' do + command "blahblah", at: 1 + command "barbar", at: 2 + end + file + end + end + + def three_hours + "0 0,3,6,9,12,15,18,21 * * *" + end +end + +class OutputJobsWithSequentialTest < Whenever::TestCase + test "defined jobs with a sequential argument" do + output = Whenever.cron \ + <<-file + every 2.hours, sequential: true do + command "blahblah" + command "foofoo" + end + file + + output_without_empty_line = lines_without_empty_line(output.lines) + assert_equal two_hours + " /bin/bash -l -c 'blahblah' ; /bin/bash -l -c 'foofoo'", output_without_empty_line.shift + end + + test "defined jobs with a sequential argument" do + output = Whenever.cron \ + <<-file + every 2.hours, sequential: true do + command "blahblah" + command "foofoo", sequence: false + end + file + + output_without_empty_line = lines_without_empty_line(output.lines) + assert_equal two_hours + " /bin/bash -l -c 'blahblah'", output_without_empty_line.shift + assert_equal two_hours + " /bin/bash -l -c 'foofoo'", output_without_empty_line.shift + end +end diff --git a/test/unit/job_test.rb b/test/unit/job_test.rb index a9e34ce5..fae54f13 100644 --- a/test/unit/job_test.rb +++ b/test/unit/job_test.rb @@ -62,7 +62,6 @@ class JobTest < Whenever::TestCase end end - class JobWithQuotesTest < Whenever::TestCase should "output the :task if it's in single quotes" do job = new_job(:template => "':task'", :task => 'abc123')