From 6bb5bdf0d3302904d06a39267bff95649ebe0037 Mon Sep 17 00:00:00 2001 From: Philipp Date: Fri, 9 Jul 2021 15:08:05 +0200 Subject: [PATCH 1/2] randomize 'at' fpr :hour and :day --- README.md | 8 +++++ lib/whenever/cron.rb | 27 +++++++++++++++ lib/whenever/job.rb | 2 +- lib/whenever/job_list.rb | 2 +- test/functional/output_random_at_test.rb | 43 ++++++++++++++++++++++++ 5 files changed, 80 insertions(+), 2 deletions(-) create mode 100644 test/functional/output_random_at_test.rb diff --git a/README.md b/README.md index ed210700..aeb916ff 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,10 @@ Run `whenever --help` for a complete list of options for selecting the schedule ### Example schedule.rb file ```ruby +# randomize all :hour- and :day-jobs, when no specific at-time is given. +# This avoids hourly jobs starting at the same time. +# set :randomize, true + every 3.hours do # 1.minute 1.day 1.week 1.month 1.year is also supported # the following tasks are run in parallel (not in sequence) runner "MyModel.some_process" @@ -71,6 +75,10 @@ every :hour do # Many shortcuts available: :hour, :day, :month, :year, :reboot runner "SomeModel.ladeeda" end +every :hour, randomize: true do # each hour, but at random minute + runner "SomeModel.ladeeda" +end + every :sunday, at: '12pm' do # Use any day of the week or :weekend, :weekday runner "Task.do_something_great" end diff --git a/lib/whenever/cron.rb b/lib/whenever/cron.rb index c1c268ca..a9d8db63 100644 --- a/lib/whenever/cron.rb +++ b/lib/whenever/cron.rb @@ -7,6 +7,10 @@ class Cron MONTHS = %w(jan feb mar apr may jun jul aug sep oct nov dec) KEYWORDS = [:reboot, :yearly, :annually, :monthly, :weekly, :daily, :midnight, :hourly] REGEX = /^(@(#{KEYWORDS.join '|'})|((\*?[\d\/,\-]*)\s){3}(\*?([\d\/,\-]|(#{MONTHS.join '|'}))*\s)(\*?([\d\/,\-]|(#{DAYS.join '|'}))*))$/i + RANGE_FOR_RANDOM_BY_TIME_UNIT = { + minute: (0...60).to_a, + hour: %w[22 23 00 01 02 03 04 05 06 07] + }.freeze attr_accessor :time, :task @@ -16,6 +20,7 @@ def initialize(time = nil, task = nil, at = nil, options = {}) @at_given = at @time = time @task = task + at = Cron.randomize_at_by_task(at, time, task) if options[:randomize] @at = at.is_a?(String) ? (Chronic.parse(at, chronic_options) || 0) : (at || 0) end @@ -42,6 +47,28 @@ def self.output(times, job, options = {}) end end + # pseudo-random: same at for same task, useful for debugging across several deploys + def self.randomize_at_by_task(at, time, task) + return at unless at.nil? + + # atm, the randomizing-feature works only for symbols :hour and :day + # return at unless time.is_a?(Symbol) && [:hour, :day].include?(time) + case time + when :hour + random_by_task(task, :minute) + when :day + random_by_task(task, :hour) + ':' + random_by_task(task, :minute).to_s + else + at + end + + end + + def self.random_by_task(task, time_unit) + array = RANGE_FOR_RANDOM_BY_TIME_UNIT[time_unit] + array[task.sum % array.length] + end + def output [time_in_cron_syntax, task].compact.join(' ').strip end diff --git a/lib/whenever/job.rb b/lib/whenever/job.rb index 2dad8329..2e1eb6d5 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, :options def initialize(options = {}) @options = options diff --git a/lib/whenever/job_list.rb b/lib/whenever/job_list.rb index 7df39c68..b5468127 100644 --- a/lib/whenever/job_list.rb +++ b/lib/whenever/job_list.rb @@ -142,7 +142,7 @@ def cron_jobs_of_time(time, jobs) 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| + Whenever::Output::Cron.output(time, job, job.options.merge(:chronic_options => @chronic_options)) do |cron| cron << "\n\n" if cron[0,1] == "@" diff --git a/test/functional/output_random_at_test.rb b/test/functional/output_random_at_test.rb new file mode 100644 index 00000000..1da95eff --- /dev/null +++ b/test/functional/output_random_at_test.rb @@ -0,0 +1,43 @@ +require 'test_helper' + +class OutputRandomAtTest < Whenever::TestCase + test "pseudo random at for hour and day" do + output = Whenever.cron \ + <<-file + set :job_template, nil + set :randomize, true + every :day do + command "blahblah" + end + file + assert_match '34 2 * * * blahblah', output + + output = Whenever.cron \ + <<-file + set :job_template, nil + every :day, randomize: true do + command "blah" + end + file + assert_match '47 5 * * * blah', output + + output = Whenever.cron \ + <<-file + set :job_template, nil + set :randomize, true + every :hour do + command "blahblah" + end + file + assert_match '34 * * * * blahblah', output + + output = Whenever.cron \ + <<-file + set :job_template, nil + every :hour, randomize: true do + command "blah" + end + file + assert_match '47 * * * * blah', output + end +end From 1de7fd1d990e788138ab010a6a2b7785350ae1b2 Mon Sep 17 00:00:00 2001 From: Philipp Date: Mon, 9 Aug 2021 10:38:05 +0200 Subject: [PATCH 2/2] randomize: consider only actual command, not path --- lib/whenever/cron.rb | 4 ++-- test/functional/output_random_at_test.rb | 10 ++++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/lib/whenever/cron.rb b/lib/whenever/cron.rb index a9d8db63..446f5025 100644 --- a/lib/whenever/cron.rb +++ b/lib/whenever/cron.rb @@ -20,7 +20,7 @@ def initialize(time = nil, task = nil, at = nil, options = {}) @at_given = at @time = time @task = task - at = Cron.randomize_at_by_task(at, time, task) if options[:randomize] + at = Cron.randomize_at_by_task(at, time, options[:task]) if options[:randomize] @at = at.is_a?(String) ? (Chronic.parse(at, chronic_options) || 0) : (at || 0) end @@ -50,7 +50,7 @@ def self.output(times, job, options = {}) # pseudo-random: same at for same task, useful for debugging across several deploys def self.randomize_at_by_task(at, time, task) return at unless at.nil? - + # atm, the randomizing-feature works only for symbols :hour and :day # return at unless time.is_a?(Symbol) && [:hour, :day].include?(time) case time diff --git a/test/functional/output_random_at_test.rb b/test/functional/output_random_at_test.rb index 1da95eff..64f105e4 100644 --- a/test/functional/output_random_at_test.rb +++ b/test/functional/output_random_at_test.rb @@ -12,6 +12,16 @@ class OutputRandomAtTest < Whenever::TestCase file assert_match '34 2 * * * blahblah', output + output = Whenever.cron \ + <<-file + set :job_template, nil + set :randomize, true + every :day do + runner "blahblah" + end + file + assert_match(/\A34 2 * * */, output) + output = Whenever.cron \ <<-file set :job_template, nil