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_cronObject



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_alignedObject



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