Class: Wurk::Cron::Parser

Inherits:
Object
  • Object
show all
Defined in:
lib/wurk/cron.rb

Overview

5-field crontab + @aliases. No seconds field, no DOW name aliases (sidekiq-ent §2.2 uses numeric DOW only).

Constant Summary collapse

FIELDS =
[
  [0, 59],
  [0, 23],
  [1, 31],
  [1, 12],
  [0, 7]
].freeze
ALIASES =
{
  '@hourly' => '0 * * * *',
  '@daily' => '0 0 * * *',
  '@midnight' => '0 0 * * *',
  '@weekly' => '0 0 * * 0',
  '@monthly' => '0 0 1 * *',
  '@yearly' => '0 0 1 1 *',
  '@annually' => '0 0 1 1 *'
}.freeze
MAX_LOOKAHEAD_MINUTES =
366 * 24 * 60 * 4
CACHE_LIMIT =

Distinct schedules a process keeps parsed. A loop is rebuilt from its hash on every tick, and a fleet has far fewer schedules than loops.

1024

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(expression) ⇒ Parser

Returns a new instance of Parser.



97
98
99
100
101
102
103
# File 'lib/wurk/cron.rb', line 97

def initialize(expression)
  parts = normalize_expression(expression)
  @expression = parts.join(' ')
  @fields = parts.each_with_index.map { |part, i| parse_field(part, *FIELDS[i]) }
  @dom_restricted = parts[2] != '*'
  @dow_restricted = parts[4] != '*'
end

Instance Attribute Details

#expression ⇒ Object (readonly)

Returns the value of attribute expression.



80
81
82
# File 'lib/wurk/cron.rb', line 80

def expression
  @expression
end

#fields ⇒ Object (readonly)

Returns the value of attribute fields.



80
81
82
# File 'lib/wurk/cron.rb', line 80

def fields
  @fields
end

Class Method Details

.cached(expression) ⇒ Object

A shared, already-parsed Parser for expression. Raises ArgumentError like new on a bad one, which is never cached. Parsers are not mutated after construction, so sharing one across loops and threads is safe.



88
89
90
91
92
93
94
95
# File 'lib/wurk/cron.rb', line 88

def self.cached(expression)
  @cache_mutex.synchronize do
    @cache.fetch(expression) do
      @cache.clear if @cache.size >= CACHE_LIMIT
      @cache[expression] = new(expression)
    end
  end
end

.resolve_zone(name) ⇒ Object



266
267
268
269
270
271
272
273
# File 'lib/wurk/cron.rb', line 266

def resolve_zone(name)
  cached = @tz_cache[name]
  return cached || nil unless cached.nil?

  @tz_cache_mutex.synchronize do
    @tz_cache.fetch(name) { @tz_cache[name] = load_zone(name) } || nil
  end
end

Instance Method Details

#local_components(epoch, tz = nil) ⇒ Object

Local wall-clock [min, hour, day, mon, dow] for epoch in tz. Public so the Loop/Poller can detect a DST fall-back fold — two distinct UTC instants that share the same local minute — and avoid a double-fire.



136
137
138
# File 'lib/wurk/cron.rb', line 136

def local_components(epoch, tz = nil)
  wall_clock(epoch, tz)
end

#match?(time, tz = nil) ⇒ Boolean

Returns:

  • (Boolean)


129
130
131
# File 'lib/wurk/cron.rb', line 129

def match?(time, tz = nil)
  match_components?(wall_clock(time.to_i, tz))
end

#next(from = ::Time.now, tz = nil) ⇒ Object

Sidekiq Ent Sidekiq::CronParser#next: the next fire Time strictly after from (default now), or nil if the schedule never matches within the lookahead window. tz evaluates the schedule in a timezone (an AS::TimeZone / TZInfo::Tz / IANA String); default is UTC. Thin wrapper over next_fire_at so there's a single crontab parser.



124
125
126
127
# File 'lib/wurk/cron.rb', line 124

def next(from = ::Time.now, tz = nil)
  epoch = next_fire_at(from.to_i, tz)
  epoch && ::Time.at(epoch)
end

#next_fire_at(from_epoch, tz = nil) ⇒ Object

Smallest UTC epoch strictly greater than from_epoch whose wall-clock components in tz match every field. Returns nil if no match within ~4 years (a malformed loop like Feb 29 in a non-leap span).



108
109
110
111
112
113
114
115
116
117
# File 'lib/wurk/cron.rb', line 108

def next_fire_at(from_epoch, tz = nil)
  t = ((from_epoch.to_i / 60) + 1) * 60
  MAX_LOOKAHEAD_MINUTES.times do
    wc = wall_clock(t, tz)
    return t if match_components?(wc)

    t += 60
  end
  nil
end