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) and prunes superseded loops for the classes it registers.
* `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:{lid}, loop-history:{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
-
.fire!(lid) ⇒ Object
Test/ops helper: fire one registered loop immediately, ignoring the leader gate and the schedule due-check.
- .jobs ⇒ Object
-
.lid(schedule, klass, options) ⇒ Object
Stable 16-hex lid from (schedule, klass, options).
-
.persist(loop_obj) ⇒ Object
pausedis written with HSETNX, not HSET: a code-declaredpaused:option is only the initial value at first registration, after which the field belongs to the runtime (Web UI pause/unpause). -
.prune_superseded(klass, keep_lids) ⇒ Object
Drop every registered loop for
klassexceptkeep_lids— the loops a booting process just registered for that class. -
.register(name, cron, worker_class, args = [], **opts) ⇒ Object
Task-stated convenience signature.
-
.reset! ⇒ Object
Test helper: wipe every Cron Redis key.
-
.unregister(lid) ⇒ Object
Drop a loop entirely: registry membership, hash, and fire history.
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!.
758 759 760 761 762 763 |
# File 'lib/wurk/cron.rb', line 758 def fire!(lid) loop_obj = LoopSet.new.fetch(lid) return nil if loop_obj.nil? Poller.new(Wurk.configuration).fire(loop_obj) 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.
674 675 676 677 |
# File 'lib/wurk/cron.rb', line 674 def lid(schedule, klass, ) opts = .is_a?(Hash) ? : {} ::Digest::SHA1.hexdigest("#{schedule}|#{klass}|#{JSON.dump(opts.sort.to_h)}")[0, 16] end |
.persist(loop_obj) ⇒ Object
paused is written with HSETNX, not HSET: a code-declared paused:
option is only the initial value at first registration, after which
the field belongs to the runtime (Web UI pause/unpause). Writing it
unconditionally un-paused a dashboard-paused loop on the next boot.
693 694 695 696 697 698 699 700 701 702 703 |
# File 'lib/wurk/cron.rb', line 693 def persist(loop_obj) fields = loop_obj.to_redis_hash paused = fields.delete('paused') key = "#{LOOP_PREFIX}#{loop_obj.lid}" Wurk.redis do |c| c.call('SADD', PERIODIC_KEY, loop_obj.lid) c.call('HSET', key, *fields.flatten) c.call('HSETNX', key, 'paused', paused) end loop_obj end |
.prune_superseded(klass, keep_lids) ⇒ Object
Drop every registered loop for klass except keep_lids — the loops a
booting process just registered for that class. Editing a schedule or
any option changes the lid, so without this the pre-edit loop stays in
periodic and keeps firing next to its replacement, forever.
Scoped to one class deliberately: the registry is fleet-wide, so a process that registers a subset of the fleet's loops (or none at all, e.g. a client-only app) must never delete loops for classes it doesn't register. Idempotent and safe to race — two processes booting the same code compute the same candidate set, and SREM/DEL repeat harmlessly.
715 716 717 718 719 720 721 722 723 |
# File 'lib/wurk/cron.rb', line 715 def prune_superseded(klass, keep_lids) keep = Array(keep_lids) stale = Wurk.redis do |c| candidates = c.call('SMEMBERS', PERIODIC_KEY) - keep candidates.select { |lid| c.call('HGET', "#{LOOP_PREFIX}#{lid}", 'klass') == klass } end stale.each { |lid| unregister(lid) } stale 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| ... }.
683 684 685 686 687 |
# File 'lib/wurk/cron.rb', line 683 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.
738 739 740 741 742 743 744 745 746 |
# File 'lib/wurk/cron.rb', line 738 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: registry membership, hash, and fire history.
Public API for retiring a loop whose register(...) line is gone (no
process registers its class any more, so prune_superseded cannot see
it); also the primitive behind pruning and ConfigTester rollback.
729 730 731 732 733 734 |
# File 'lib/wurk/cron.rb', line 729 def unregister(lid) Wurk.redis do |c| c.call('SREM', PERIODIC_KEY, lid) c.call('DEL', "#{LOOP_PREFIX}#{lid}", "#{HISTORY_PREFIX}#{lid}") end end |