Class: Hammer::Cron
- Inherits:
-
Object
- Object
- Hammer::Cron
- Defined in:
- lib/hammer/cron.rb
Overview
Schedule of a task declared with cron '<expr>'. Parses the
expression once (at Hammerfile eval, so typos fail at load time) and
answers the scheduler's questions: does this wall-clock minute match,
is a run due given the last one, and when is the next run.
Accepted forms:
cron '*/10 * * * *' # standard 5-field crontab
cron '10m' # simple interval: every 10 minutes
cron '10s' / '2h' / '1d' # seconds / hours / days
cron '@daily' # shortcut, expands to '0 0 * * *'
Cron fields support '', 'a', 'a-b', '/n', 'a-b/n' and comma lists, numeric values only (no 'jan'/'mon' names). Weekday 7 equals 0 (Sunday). Intervals are measured from the last run - not aligned to the clock - which makes odd periods like '90m' possible and keeps restarts from re-firing (last run persists in the state file).
Instance Attribute Summary collapse
-
#interval ⇒ Object
readonly
Returns the value of attribute interval.
-
#source ⇒ Object
readonly
Returns the value of attribute source.
Instance Method Summary collapse
-
#due?(tick, last_run) ⇒ Boolean
Should the scheduler fire at
tick(a boundary-aligned Time), given the persisted last run? Cron mode fires once per matching minute; interval mode fires when a full interval has elapsed (or the job never ran, so a fresh job gives immediate feedback on the first tick). -
#initialize(expr) ⇒ Cron
constructor
A new instance of Cron.
- #interval? ⇒ Boolean
-
#matches?(t) ⇒ Boolean
True when local wall-clock time
t(minute resolution) matches the cron expression. -
#next_run(from = Time.now, last_run: nil) ⇒ Object
Next fire time strictly after
from.
Constructor Details
#initialize(expr) ⇒ Cron
Returns a new instance of Cron.
37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 |
# File 'lib/hammer/cron.rb', line 37 def initialize(expr) @source = expr.to_s.strip if (m = @source.match(/\A(\d+)([smhd])\z/)) @interval = m[1].to_i * INTERVAL_UNITS[m[2]] raise Hammer::Error, "cron: interval must be > 0 in #{@source.inspect}" if @interval.zero? return end if @source.start_with?('@') && !SHORTCUTS.key?(@source) raise Hammer::Error, "cron: unknown shortcut #{@source.inspect} (valid: #{SHORTCUTS.keys.join(', ')})" end parts = (SHORTCUTS[@source] || @source).split(/\s+/) unless parts.size == 5 raise Hammer::Error, "cron: #{@source.inspect} - expected 5 fields ('*/10 * * * *'), " \ "an interval ('10s', '10m', '2h', '1d') or a shortcut (#{SHORTCUTS.keys.join(', ')})" end @fields = FIELDS.each_with_index.map { |(name, lo, hi), i| parse_field(name, parts[i], lo, hi) } @dom_star = parts[2] == '*' @dow_star = parts[4] == '*' end |
Instance Attribute Details
#interval ⇒ Object (readonly)
Returns the value of attribute interval.
35 36 37 |
# File 'lib/hammer/cron.rb', line 35 def interval @interval end |
#source ⇒ Object (readonly)
Returns the value of attribute source.
35 36 37 |
# File 'lib/hammer/cron.rb', line 35 def source @source end |
Instance Method Details
#due?(tick, last_run) ⇒ Boolean
Should the scheduler fire at tick (a boundary-aligned Time),
given the persisted last run? Cron mode fires once per matching
minute; interval mode fires when a full interval has elapsed (or
the job never ran, so a fresh job gives immediate feedback on the
first tick).
83 84 85 86 87 88 89 |
# File 'lib/hammer/cron.rb', line 83 def due?(tick, last_run) if interval? last_run.nil? || tick.to_i - last_run.to_i >= @interval else matches?(tick) && (last_run.nil? || last_run.to_i / 60 < tick.to_i / 60) end end |
#interval? ⇒ Boolean
61 62 63 |
# File 'lib/hammer/cron.rb', line 61 def interval? !@interval.nil? end |
#matches?(t) ⇒ Boolean
True when local wall-clock time t (minute resolution) matches the
cron expression. Vixie-cron day rule: when BOTH day-of-month and
day-of-week are restricted, matching either one is enough;
otherwise both must match.
69 70 71 72 73 74 75 76 |
# File 'lib/hammer/cron.rb', line 69 def matches?(t) return false if interval? min, hour, dom, mon, dow = @fields return false unless min[t.min] && hour[t.hour] && mon[t.month] dom_ok = dom[t.day] dow_ok = dow[t.wday] @dom_star || @dow_star ? dom_ok && dow_ok : dom_ok || dow_ok end |
#next_run(from = Time.now, last_run: nil) ⇒ Object
Next fire time strictly after from. Cron mode walks minute by
minute - real-world expressions match within days, and the walk is
~100 hash lookups per simulated minute - capped at 500 days so an
impossible date (a Feb 30 style expression) fails loudly instead of
spinning forever.
96 97 98 99 100 101 102 103 104 105 106 107 108 |
# File 'lib/hammer/cron.rb', line 96 def next_run(from = Time.now, last_run: nil) if interval? return from if last_run.nil? Time.at(last_run.to_i + @interval) else t = Time.at((from.to_i / 60 + 1) * 60) (500 * 24 * 60).times do return t if matches?(t) t += 60 end raise Hammer::Error, "cron: #{@source.inspect} never matches" end end |