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!`.



697
698
699
700
701
702
# File 'lib/wurk/cron.rb', line 697

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

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

.jobsObject



687
688
689
# File 'lib/wurk/cron.rb', line 687

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.



642
643
644
645
# File 'lib/wurk/cron.rb', line 642

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



657
658
659
660
661
662
663
# File 'lib/wurk/cron.rb', line 657

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| … }`.



651
652
653
654
655
# File 'lib/wurk/cron.rb', line 651

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.



677
678
679
680
681
682
683
684
685
# File 'lib/wurk/cron.rb', line 677

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.



668
669
670
671
672
673
# File 'lib/wurk/cron.rb', line 668

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