Module: DevDoc::Test::Lints::CronSchedule
- Defined in:
- lib/dev_doc/test/lints/cron_schedule.rb
Overview
Reject cron schedules that fire less often than once per day (configurable). Anything weekly or longer is always wrong.
## Rationale Long-interval cron jobs fail catastrophically when missed. Sidekiq-cron does not replay missed occurrences — if the server is down at Mon 3am, the next fire is the following Mon 3am. A single server restart at the wrong moment costs a full interval of staleness. Same in production: transient Sidekiq issues, deploys, or scheduler glitches all cause the same outage.
The recommended pattern is a frequent cron + persisted last-run timestamp (“self-throttled cron”). Cron fires often; the job itself decides whether to do the work based on a ‘fetched_at`-style column. Fresh? Fast no-op. Stale? Run the actual work. Missed runs self-heal within the cron interval.
The cron interval must be strictly less than the job’s desired refresh cadence (its staleness threshold). One missed cron fire then only adds ≤ 1 cron interval of staleness, never doubling the cadence.
Desired refresh | Right cron | Wrong cron
----------------+---------------------------+-----------
Weekly | Daily (or shorter) | Weekly
Daily | Hourly (or shorter) | Daily
Hourly | Every 15 min (or shorter) | Hourly
Weekly+ crons are always wrong, regardless of staleness threshold: no useful threshold can exceed the cron interval, so the self-throttle mechanism can’t work. This lint enforces that. A second test (‘test_staleness_threshold_aligned`) also verifies that each job’s ‘STALENESS_THRESHOLD` constant (or a configurable name) is strictly greater than the cron interval — catching the “inverted pattern” where every cron fire does the work because the threshold is equal to or shorter than the interval.
❌
myki_fetch:
cron: "0 3 * * 1" # weekly — one miss = 7 days stale
class: MykiFetchJob
✔️
myki_fetch:
cron: "0 3 * * *" # daily; MykiAccount#stale?(7.days) gates work
class: MykiFetchJob
See ‘best_practices/backend/en/08_job.md#cron-schedule-frequency`.
## Usage Include this module in a Minitest test class to get the ‘test_no_long_interval_cron` and `test_staleness_threshold_aligned` methods. To override the cap, redefine `MAX_INTERVAL_SECONDS` on the test class. To change the constant name searched in job files, redefine `STALENESS_CONSTANT_NAME`.
class CronScheduleTest < ActiveSupport::TestCase
include DevDoc::Test::Lints::CronSchedule
# MAX_INTERVAL_SECONDS = 3600 # tighten to 1h
# STALENESS_CONSTANT_NAME = 'STALE_AFTER' # different convention
end
Constant Summary collapse
- MAX_INTERVAL_SECONDS =
Default cap. Per-project override: redefine the constant on the test class that includes this module.
CronScheduleChecker::DEFAULT_MAX_INTERVAL_SECONDS
- STALENESS_CONSTANT_NAME =
Name of the constant to look up in job source files. Per-project override: redefine on the test class that includes this module.
CronScheduleChecker::DEFAULT_STALENESS_CONSTANT
Instance Method Summary collapse
Instance Method Details
#test_no_long_interval_cron ⇒ Object
301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 |
# File 'lib/dev_doc/test/lints/cron_schedule.rb', line 301 def test_no_long_interval_cron max = self.class::MAX_INTERVAL_SECONDS result = CronScheduleChecker .new(schedules_path, max_interval_seconds: max) .offenders skip "no #{schedules_path} found" if result == CronScheduleChecker::MissingFile assert result.empty?, "Cron schedules must be valid and fire at least every " \ "#{CronScheduleChecker.format_seconds(max)}. Offenders:\n" \ "#{result.join("\n")}\n\n" \ "Use a frequent cron + a persisted staleness check in the job. " \ 'See https://github.com/hgani/dev-doc/blob/main/best_practices/backend/en/08_job.md#cron-schedule-frequency' end |
#test_staleness_threshold_aligned ⇒ Object
317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 |
# File 'lib/dev_doc/test/lints/cron_schedule.rb', line 317 def test_staleness_threshold_aligned constant = self.class::STALENESS_CONSTANT_NAME result = CronScheduleChecker .new(schedules_path) .staleness_offenders(jobs_path: jobs_path, constant: constant) skip "no #{schedules_path} found" if result == CronScheduleChecker::MissingFile assert result.empty?, "Each job's #{constant} must be strictly greater than its cron " \ "interval (otherwise every cron fire does the work and the " \ "staleness check is useless). Offenders:\n" \ "#{result.join("\n")}\n\n" \ 'See https://github.com/hgani/dev-doc/blob/main/best_practices/backend/en/08_job.md#cron-schedule-frequency' end |