Module: Wurk::Cron

Defined in:
lib/wurk/cron.rb

Overview

Sidekiq Enterprise periodic jobs. Pure leader-driven cron — only the elected leader enqueues per tick; followers run nothing. No backfill on restart. DST-aware via per-loop timezone. In-tree crontab parser (no fugit dependency) supporting 5-field expressions plus the standard ‘@hourly` / `@daily` / `@weekly` / `@monthly` / `@yearly` aliases.

Spec: docs/target/sidekiq-ent.md §2.

Layout:

* `Cron::Parser` — crontab → wall-clock match + `next_fire_at`. Walks
  forward minute-by-minute in the loop's TZ; DST gaps are skipped
  naturally because the wall-clock components advance past them.
* `Cron::Loop` — one registered job. Identity = SHA1(schedule+klass+opts)
  so a re-registration of the same loop is idempotent.
* `Cron::Manager` — registration DSL. `mgr.register(cron, klass, **opts)`
  with `tz=` mass-setter. Writes to Redis (`periodic` SET + `loops:{lid}`
  HASH).
* `Cron::LoopSet` — Enumerable view (`each`/`size`/`fetch(lid)`).
* `Cron::ConfigTester` — boot-time validator. Verifies cron syntax and
  that every worker class constant resolves.
* `Cron::Poller` — once-per-minute tick loop. Only the cluster leader
  (`Component#leader?` / `dear-leader`) enqueues; non-leaders return
  early without iterating loops.

Wire-compat: ‘periodic`, `loops:Cron.lid`, `loop-history:Cron.lid` per docs/target/sidekiq-ent.md §2.7. Periodic enqueue is gated by the single cluster leader (§6, `Component#leader?`) rather than a separate `cron-leader` lock — see the §2.7 divergence note.

Defined Under Namespace

Classes: ConfigTester, Loop, LoopSet, Manager, Parser, Poller

Constant Summary collapse

PERIODIC_KEY =
'periodic'
LOOP_PREFIX =
'loops:'
HISTORY_PREFIX =
'loop-history:'
HISTORY_CAP =
25
DEFAULT_TICK_SECONDS =
60
MISSED_TICK_THRESHOLD =
90

Class Method Summary collapse

Class Method Details

.fire!(lid) ⇒ Object

Test/ops helper: fire one registered loop immediately, ignoring the leader gate and the schedule due-check. Records history + advances the fire marks just like a leader tick, so specs can assert on the enqueue and history deterministically without waiting on wall-clock or stubbing leadership. Returns the enqueued jid, or nil for an unknown lid. Aliased as ‘Sidekiq::Periodic.fire!`.



626
627
628
629
630
631
# File 'lib/wurk/cron.rb', line 626

def fire!(lid)
  loop_obj = LoopSet.new.fetch(lid)
  return nil if loop_obj.nil?

  Poller.new(Wurk.configuration).fire(loop_obj)
end

.jobsObject



616
617
618
# File 'lib/wurk/cron.rb', line 616

def jobs
  LoopSet.new
end

.lid(schedule, klass, options) ⇒ Object

Stable 16-hex lid from (schedule, klass, options). Re-registering the same triple no-ops because the Redis writes overwrite under the same key.



571
572
573
574
# File 'lib/wurk/cron.rb', line 571

def lid(schedule, klass, options)
  opts = options.is_a?(Hash) ? options : {}
  ::Digest::SHA1.hexdigest("#{schedule}|#{klass}|#{JSON.dump(opts.sort.to_h)}")[0, 16]
end

.persist(loop_obj) ⇒ Object



586
587
588
589
590
591
592
# File 'lib/wurk/cron.rb', line 586

def persist(loop_obj)
  Wurk.redis do |c|
    c.call('SADD', PERIODIC_KEY, loop_obj.lid)
    c.call('HSET', "#{LOOP_PREFIX}#{loop_obj.lid}", *loop_obj.to_redis_hash.flatten)
  end
  loop_obj
end

.register(name, cron, worker_class, args = [], **opts) ⇒ Object

Task-stated convenience signature. ‘name` is treated as a label; the lid is still derived from (schedule, klass, opts) so the call is idempotent. Callers that want the Sidekiq DSL should use `Manager#register` via `config.periodic { |mgr| … }`.



580
581
582
583
584
# File 'lib/wurk/cron.rb', line 580

def register(name, cron, worker_class, args = [], **opts)
  merged = opts.merge(args: args)
  merged[:label] = name if name
  Loop.new(schedule: cron, klass: worker_class.to_s, options: merged).tap { |lp| persist(lp) }
end

.reset!Object

Test helper: wipe every Cron Redis key. Production code must not call this — it removes every registered loop in the cluster.



606
607
608
609
610
611
612
613
614
# File 'lib/wurk/cron.rb', line 606

def reset!
  Wurk.redis do |c|
    lids = c.call('SMEMBERS', PERIODIC_KEY)
    lids.each do |lid|
      c.call('DEL', "#{LOOP_PREFIX}#{lid}", "#{HISTORY_PREFIX}#{lid}")
    end
    c.call('DEL', PERIODIC_KEY)
  end
end

.unregister(lid) ⇒ Object

Drop a loop entirely. Used by the Web UI delete action and by the config-reload path so a removed ‘register(…)` line vanishes from Redis on next boot.



597
598
599
600
601
602
# File 'lib/wurk/cron.rb', line 597

def unregister(lid)
  Wurk.redis do |c|
    c.call('SREM', PERIODIC_KEY, lid)
    c.call('DEL', "#{LOOP_PREFIX}#{lid}", "#{HISTORY_PREFIX}#{lid}")
  end
end