Class: Wurk::Cron::Poller
- Inherits:
-
Object
- Object
- Wurk::Cron::Poller
- Includes:
- Wurk::Component
- Defined in:
- lib/wurk/cron.rb
Overview
Once-per-minute tick driver, plus one tick the moment this process wins the
lock: at a tick's phase, a leader that lives under a minute would never tick.
Only the cluster leader iterates the LoopSet and enqueues; non-leaders return
early. A slot that wall-clock has passed by more than MISSED_TICK_THRESHOLD
seconds is skipped with a warning, never backfilled, and the occurrence due
now, if any, fires in its place.
Instance Attribute Summary collapse
-
#config ⇒ Object
included
from Wurk::Component
readonly
Returns the value of attribute config.
Instance Method Summary collapse
- #default_tag(dir = Dir.pwd) ⇒ Object included from Wurk::Component
-
#enqueue_if_due(loop_obj, marks = nil) ⇒ Object
The marks advance before the push, via CAS: the leader gate is a cached read, so a second poller can reach the same due loop for a few seconds after a handover.
-
#fire(loop_obj) ⇒ Object
Fire one loop right now, bypassing both the leader gate and the schedule due-check, recording history + advancing the fire marks exactly like a real tick.
-
#fire_event(event, oneshot: true, reverse: false, reraise: false) ⇒ Object
included
from Wurk::Component
Invokes lifecycle hooks for
event. - #handle_exception(ex, ctx = {}) ⇒ Object included from Wurk::Component
- #hostname ⇒ Object included from Wurk::Component
- #identity ⇒ Object included from Wurk::Component
-
#initialize(config) ⇒ Poller
constructor
A new instance of Poller.
-
#leader? ⇒ Boolean
included
from Wurk::Component
True iff this process currently holds the cluster
dear-leaderlock. -
#logger ⇒ Object
included
from Wurk::Component
--- delegated to config -------------------------------------------.
- #mono_ms ⇒ Object included from Wurk::Component
- #process_nonce ⇒ Object included from Wurk::Component
-
#real_ms ⇒ Object
included
from Wurk::Component
--- clocks ---------------------------------------------------------.
- #redis(idempotent: false) ⇒ Object included from Wurk::Component
-
#safe_thread(name, priority: nil, &block) ⇒ Object
included
from Wurk::Component
Spawns a named thread that runs
blockunderwatchdog(name). -
#start ⇒ Object
The loop waits one interval before its first tick unless this process wins the lock first: let a process that never leads, or exits at once, exit without ticking at all.
-
#terminate ⇒ Object
Blocks until the thread is really gone: the launcher releases the cluster lock immediately after this returns, and a tick still in flight would enqueue loops the next leader is about to fire itself.
-
#tick ⇒ Object
Leader-gated by the single cluster lock (Component#leader? reads
dear-leader): non-leaders return early and never iterate the LoopSet. - #tid ⇒ Object included from Wurk::Component
-
#watchdog(last_words) ⇒ Object
included
from Wurk::Component
Wraps a block at a thread boundary: any unhandled exception is reported via handle_exception (so it lands in error_handlers / the log) and then re-raised.
Constructor Details
#initialize(config) ⇒ Poller
Returns a new instance of Poller.
704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 |
# File 'lib/wurk/cron.rb', line 704 def initialize(config) @config = config @client = Client.new(config: config) @thread = nil # Operators never need to touch this; integration tests shrink it so a # due loop fires within the test window instead of waiting a full minute. @tick_interval = config[:cron_tick_interval] || DEFAULT_TICK_SECONDS @done = false @woken = false @mutex = ::Mutex.new @sleeper = ::ConditionVariable.new @on_leader = method(:wake_for_leadership) @unknown_reported = ::Set.new @derived = {} end |
Instance Attribute Details
#config ⇒ Object (readonly) Originally defined in module Wurk::Component
Returns the value of attribute config.
Instance Method Details
#default_tag(dir = Dir.pwd) ⇒ Object Originally defined in module Wurk::Component
#enqueue_if_due(loop_obj, marks = nil) ⇒ Object
The marks advance before the push, via CAS: the leader gate is a cached read, so a second poller can reach the same due loop for a few seconds after a handover. Losing the CAS means another tick owns this slot — enqueue nothing.
A slot older than the missed-tick threshold is not backfilled (ent §2.6: no backfill on restart, missed ticks across a leader gap are dropped): the marks still advance past it, but nothing is pushed.
One claim, one push at most, whichever slot it pushes for: the occurrence due now stands in for a stale one under the stale one's mark, so a poller that read the same mark loses the CAS for both.
marks is the raw [lf, nf] pair the caller just read (a tick's
LoopSet read); without it they are read here.
786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 |
# File 'lib/wurk/cron.rb', line 786 def enqueue_if_due(loop_obj, marks = nil) return if loop_obj.paused? now = ::Time.now.to_i slot, mark = due_slot(loop_obj, now, marks) return if slot.nil? firing = slot_to_fire(loop_obj, slot, now) future = loop_obj.next_fire_after(firing || slot, now) return unless claim_fire?(loop_obj, mark, now, future) skip_missed(loop_obj, slot, now) unless firing == slot return if firing.nil? fire_claimed(loop_obj, now) end |
#fire(loop_obj) ⇒ Object
Fire one loop right now, bypassing both the leader gate and the schedule due-check, recording history + advancing the fire marks exactly like a real tick. Powers Cron.fire! (deterministic specs / manual "run now"); the scheduled, leader-gated path stays #tick.
807 808 809 810 811 812 |
# File 'lib/wurk/cron.rb', line 807 def fire(loop_obj) now = ::Time.now.to_i jid = enqueue!(loop_obj) record_fire(loop_obj, jid, now, loop_obj.next_fire_at(now)) jid end |
#fire_event(event, oneshot: true, reverse: false, reraise: false) ⇒ Object Originally defined in module Wurk::Component
Invokes lifecycle hooks for event. Hooks run in registration order
(or LIFO when reverse: true, used for teardown). A raise in one hook
is reported via handle_exception and does NOT stop the next hook unless
reraise: true (used in tests / fail-fast boot). oneshot: true
clears the bucket after dispatch so the event can't fire twice.
#handle_exception(ex, ctx = {}) ⇒ Object Originally defined in module Wurk::Component
#hostname ⇒ Object Originally defined in module Wurk::Component
#identity ⇒ Object Originally defined in module Wurk::Component
#leader? ⇒ Boolean Originally defined in module Wurk::Component
True iff this process currently holds the cluster dear-leader lock.
Cached per Component instance for LEADER_CACHE_TTL_MS (~5s): cron and
the metrics rollups call this every tick, and an uncached GET would
double their Redis traffic at short intervals for no benefit — the
lock's own renewal cadence (60s+, spec §6.1) easily tolerates a
few-second-stale read. Returns false unconditionally when
WURK_LEADER=false (or SIDEKIQ_LEADER=false) is set on the process
(opt-out hot-standby). Any Redis error is swallowed → false, so a
transient partition can't propagate as an exception into user code.
Spec: docs/target/sidekiq-ent.md §6.1.
#logger ⇒ Object Originally defined in module Wurk::Component
--- delegated to config -------------------------------------------
#mono_ms ⇒ Object Originally defined in module Wurk::Component
#process_nonce ⇒ Object Originally defined in module Wurk::Component
#real_ms ⇒ Object Originally defined in module Wurk::Component
--- clocks ---------------------------------------------------------
#redis(idempotent: false) ⇒ Object Originally defined in module Wurk::Component
#safe_thread(name, priority: nil, &block) ⇒ Object Originally defined in module Wurk::Component
Spawns a named thread that runs block under watchdog(name). The
parent must retain the returned Thread; otherwise GC may not, but
report_on_exception is disabled so we don't double-log on death.
Priority resolution matches Sidekiq (component.rb:44-48): explicit
argument, then config.thread_priority, then -1. Ruby's default of 0
buys a 100ms timeslice; each negative step halves it, so -1 keeps a
CPU-heavy capsule from starving its siblings for a whole tick.
#start ⇒ Object
The loop waits one interval before its first tick unless this process wins the lock first: let a process that never leads, or exits at once, exit without ticking at all.
723 724 725 726 727 728 729 |
# File 'lib/wurk/cron.rb', line 723 def start return @thread if @thread @mutex.synchronize { @done = false } leader_hooks << @on_leader @thread = safe_thread('cron-poller') { run_loop } end |
#terminate ⇒ Object
Blocks until the thread is really gone: the launcher releases the cluster lock immediately after this returns, and a tick still in flight would enqueue loops the next leader is about to fire itself.
Cleared only on a confirmed join (Thread#join returns nil on timeout): a wedged thread must stay tracked so #start's guard returns it instead of clearing @done, which would un-terminate the loop it is still inside and leave two tick threads double-enqueuing the same loops.
739 740 741 742 743 744 745 746 |
# File 'lib/wurk/cron.rb', line 739 def terminate leader_hooks.delete(@on_leader) @mutex.synchronize do @done = true @sleeper.signal end @thread = nil if @thread&.join(TimerLoop::JOIN_TIMEOUT) end |
#tick ⇒ Object
Leader-gated by the single cluster lock (Component#leader? reads
dear-leader): non-leaders return early and never iterate the LoopSet.
The Launcher owns the lock's renewal — the poller no longer runs (or
expires) its own.
Rescued per loop: the LoopSet order is stable, so one loop that raises on every tick (a push its middleware refuses, a class that no longer loads) would otherwise starve every loop after it, forever.
756 757 758 759 760 761 762 763 764 765 766 767 768 769 |
# File 'lib/wurk/cron.rb', line 756 def tick return unless leader? seen = ::Set.new LoopSet.new(@config).each do |lp| seen << derived_key(lp) enqueue_if_due(lp, lp.fire_marks) rescue StandardError => e handle_exception(e, { context: 'cron-poller', lid: lp.lid, klass: lp.klass }) end @derived.select! { |key, _| seen.include?(key) } rescue StandardError => e handle_exception(e, { context: 'cron-poller' }) end |
#tid ⇒ Object Originally defined in module Wurk::Component
#watchdog(last_words) ⇒ Object Originally defined in module Wurk::Component
Wraps a block at a thread boundary: any unhandled exception is reported
via handle_exception (so it lands in error_handlers / the log) and then
re-raised. last_words is the component label included in the context.