Class: DevDoc::Test::Lints::CronScheduleChecker
- Inherits:
-
Object
- Object
- DevDoc::Test::Lints::CronScheduleChecker
- Defined in:
- lib/dev_doc/test/lints/cron_schedule.rb
Overview
Framework-agnostic check: reads a sidekiq-cron-style ‘schedules.yml` and returns a list of offender descriptions for any cron expression that fires less often than `max_interval_seconds`, and optionally checks that each job’s staleness threshold is greater than the cron interval.
Wrapped by the Minitest module ‘CronSchedule` below — see that module for the rationale and examples. Tests cover the checker directly so they don’t have to mock a test framework.
Constant Summary collapse
- DEFAULT_MAX_INTERVAL_SECONDS =
1 day. Weekly (and longer) crons are always wrong: no staleness threshold can usefully exceed the cron interval, so a weekly cron can’t be paired with a meaningful self-throttle. Daily cron is the most generous interval the pattern supports (works for weekly-desired refreshes like Myki). Hourly+ is always safe.
86_400- MissingFile =
Sentinel returned by ‘#offenders` when the schedules file doesn’t exist. Distinguishable from ‘[]` (file present, all good) so the Minitest wrapper can `skip` rather than `assert`.
Module.new
- DEFAULT_STALENESS_CONSTANT =
Default constant name to look up in job source files when checking that the staleness threshold is larger than the cron interval.
'STALENESS_THRESHOLD'.freeze
- SAMPLE_ANCHOR =
Anchored reference for sampling cron fires. A fixed Monday so weekday-restricted crons behave the same regardless of when the test suite runs.
Time.utc(2025, 1, 6).freeze
- SAMPLE_COUNT =
How many consecutive fires to sample. 100 covers:
- sub-hourly crons (max gap surfaces within minutes) - business-hours patterns like `0 9-17 * * *` (overnight gap visible after the first day) - weekly/monthly (max gap visible within a few cycles) - yearly (still gets ≥2 fires) 100
Class Method Summary collapse
Instance Method Summary collapse
-
#initialize(schedules_path, max_interval_seconds: DEFAULT_MAX_INTERVAL_SECONDS) ⇒ CronScheduleChecker
constructor
A new instance of CronScheduleChecker.
-
#offenders ⇒ Object
Returns an Array<String> of offender descriptions (one per invalid or too-infrequent cron), ‘[]` if all crons are compliant, or `MissingFile` if the schedules file doesn’t exist.
-
#staleness_offenders(jobs_path: nil, constant: DEFAULT_STALENESS_CONSTANT) ⇒ Object
Checks that each job’s staleness threshold constant is strictly greater than the cron interval.
Constructor Details
#initialize(schedules_path, max_interval_seconds: DEFAULT_MAX_INTERVAL_SECONDS) ⇒ CronScheduleChecker
Returns a new instance of CronScheduleChecker.
33 34 35 36 |
# File 'lib/dev_doc/test/lints/cron_schedule.rb', line 33 def initialize(schedules_path, max_interval_seconds: DEFAULT_MAX_INTERVAL_SECONDS) @schedules_path = schedules_path @max_interval_seconds = max_interval_seconds end |
Class Method Details
.format_seconds(seconds) ⇒ Object
105 106 107 108 109 110 111 |
# File 'lib/dev_doc/test/lints/cron_schedule.rb', line 105 def self.format_seconds(seconds) return "#{seconds}s" if seconds < 60 return "#{seconds / 60}m" if seconds < 3600 return "#{seconds / 3600}h" if seconds < 86_400 "#{seconds / 86_400}d" end |
Instance Method Details
#offenders ⇒ Object
Returns an Array<String> of offender descriptions (one per invalid or too-infrequent cron), ‘[]` if all crons are compliant, or `MissingFile` if the schedules file doesn’t exist.
42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 |
# File 'lib/dev_doc/test/lints/cron_schedule.rb', line 42 def offenders return MissingFile unless @schedules_path.exist? # YAML.safe_load returns nil for an empty file. (YAML.safe_load(@schedules_path.read) || {}).each_with_object([]) do |(name, opts), acc| next unless opts.is_a?(Hash) && (cron = opts['cron']) interval = interval_seconds_for(cron) if interval == :invalid acc << " #{name}: invalid cron expression (cron: #{cron.inspect})" next end next if interval.nil? || interval <= @max_interval_seconds acc << " #{name}: every #{self.class.format_seconds(interval)} (cron: #{cron.inspect})" end end |
#staleness_offenders(jobs_path: nil, constant: DEFAULT_STALENESS_CONSTANT) ⇒ Object
Checks that each job’s staleness threshold constant is strictly greater than the cron interval. Returns an Array<String> of offender descriptions, ‘[]` if all are compliant, or `MissingFile` if the schedules file doesn’t exist.
Parameters:
jobs_path - Pathname/String pointing to `app/jobs/` (or any
directory tree to search for job source files).
If nil or the path doesn't exist, returns `[]`
(nothing to check — silently skipped so projects
without job-level constants don't have to opt out).
constant - Name of the constant to look up (default:
`STALENESS_THRESHOLD`). Projects that use a
different convention (e.g. `STALE_AFTER`) can
override this.
The constant value is extracted by grepping the job source files for the pattern ‘CONSTANT = <number>.<unit>` (e.g. `1.hour`, `45.minutes`). Jobs that use a per-record stale? method instead of a job-level constant produce no constant to inspect and are skipped silently.
80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 |
# File 'lib/dev_doc/test/lints/cron_schedule.rb', line 80 def staleness_offenders(jobs_path: nil, constant: DEFAULT_STALENESS_CONSTANT) return MissingFile unless @schedules_path.exist? return [] if jobs_path.nil? jobs_root = Pathname.new(jobs_path) return [] unless jobs_root.exist? (YAML.safe_load(@schedules_path.read) || {}).each_with_object([]) do |(name, opts), acc| next unless opts.is_a?(Hash) && (cron = opts['cron']) && (class_name = opts['class']) interval = interval_seconds_for(cron) next if interval == :invalid || interval.nil? threshold = staleness_threshold_for(class_name, jobs_root, constant) next if threshold.nil? # no constant found — skip silently next if interval < threshold acc << " #{name} (#{class_name}): cron fires every " \ "#{self.class.format_seconds(interval)} but #{constant} is " \ "#{self.class.format_seconds(threshold)} — " \ 'cron interval must be strictly less than the staleness threshold' end end |