Class: Wurk::Metrics::QueueRollup
- Inherits:
-
Object
- Object
- Wurk::Metrics::QueueRollup
- Includes:
- Component
- Defined in:
- lib/wurk/metrics/queue_rollup.rb
Overview
Leader-only background thread that snapshots each queue's current depth (LLEN) and head-of-line latency into compact per-queue gauge buckets the dashboard's "queue size / latency over time" charts read directly:
qm|1m|<epoch> HASH {<queue>|sz, <queue>|lt} TTL 24h
qm|5m|<epoch> HASH {<queue>|sz, <queue>|lt} TTL 7d
qm|1h|<epoch> HASH {<queue>|sz, <queue>|lt} TTL 30d
Unlike Metrics::Rollup (which SUMS counters rolled up from a source),
size and latency are GAUGES — point-in-time values — so each tick samples
"now" and writes it to the current bucket at every resolution. Within a
coarse (5m/1h) bucket the per-minute ticks overwrite, so the bucket holds
the latest sample in its window (a "last value" downsample, which is the
right summary for a gauge). Leader-gated so N workers don't each sample
the same queues every minute. <epoch> is the UTC start-of-bucket.
Spec: docs/target/sidekiq-ent.md §5.2 (sidekiq.queue.size / sidekiq.queue.latency gauges), §7 Historical tab.
Constant Summary collapse
- PREFIX =
'qm'- SIZE_KIND =
'sz'- LAT_KIND =
'lt'- BUCKETS =
Mirror Metrics::Rollup retention so the dashboard's range selector (24h·1m / 7d·5m / 30d·1h) maps 1:1 to both the throughput and the queue-gauge series.
Wurk::Metrics::Rollup::BUCKETS
- DEFAULT_TICK_SECONDS =
60
Instance Attribute Summary collapse
-
#config ⇒ Object
included
from Component
readonly
Returns the value of attribute config.
Class Method Summary collapse
Instance Method Summary collapse
- #default_tag(dir = Dir.pwd) ⇒ Object included from Component
-
#fire_event(event, oneshot: true, reverse: false, reraise: false) ⇒ Object
included
from Component
Invokes lifecycle hooks for
event. - #handle_exception(ex, ctx = {}) ⇒ Object included from Component
- #hostname ⇒ Object included from Component
- #identity ⇒ Object included from Component
-
#initialize(config) ⇒ QueueRollup
constructor
A new instance of QueueRollup.
-
#leader? ⇒ Boolean
included
from Component
True iff this process currently holds the cluster
dear-leaderlock. -
#logger ⇒ Object
included
from Component
--- delegated to config -------------------------------------------.
- #mono_ms ⇒ Object included from Component
- #process_nonce ⇒ Object included from Component
-
#real_ms ⇒ Object
included
from Component
--- clocks ---------------------------------------------------------.
- #redis(idempotent: false) ⇒ Object included from Component
-
#safe_thread(name, priority: nil, &block) ⇒ Object
included
from Component
Spawns a named thread that runs
blockunderwatchdog(name). -
#sample(now = ::Time.now) ⇒ Object
One sampling pass, bypassing the leader gate and the sleep loop.
- #start ⇒ Object
-
#terminate ⇒ Object
Blocks until the thread is really gone: the launcher releases the cluster lock immediately after this returns, and a sample still in flight would write the same buckets as the next leader's first one.
-
#tick(now: ::Time.now) ⇒ Object
Leader-gated: only the elected leader samples, so N workers don't each HSET the same buckets every minute.
- #tid ⇒ Object included from Component
-
#watchdog(last_words) ⇒ Object
included
from 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) ⇒ QueueRollup
Returns a new instance of QueueRollup.
47 48 49 50 51 |
# File 'lib/wurk/metrics/queue_rollup.rb', line 47 def initialize(config) @config = config @timer = TimerLoop.new(config[:metrics_rollup_interval] || DEFAULT_TICK_SECONDS) @thread = nil end |
Instance Attribute Details
#config ⇒ Object (readonly) Originally defined in module Component
Returns the value of attribute config.
Class Method Details
.bucket_key(bucket, epoch) ⇒ Object
43 44 45 |
# File 'lib/wurk/metrics/queue_rollup.rb', line 43 def self.bucket_key(bucket, epoch) "#{PREFIX}|#{bucket}|#{epoch}" end |
Instance Method Details
#default_tag(dir = Dir.pwd) ⇒ Object Originally defined in module Component
#fire_event(event, oneshot: true, reverse: false, reraise: false) ⇒ Object Originally defined in module 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 Component
#hostname ⇒ Object Originally defined in module Component
#identity ⇒ Object Originally defined in module Component
#leader? ⇒ Boolean Originally defined in module 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 Component
--- delegated to config -------------------------------------------
#mono_ms ⇒ Object Originally defined in module Component
#process_nonce ⇒ Object Originally defined in module Component
#real_ms ⇒ Object Originally defined in module Component
--- clocks ---------------------------------------------------------
#redis(idempotent: false) ⇒ Object Originally defined in module Component
#safe_thread(name, priority: nil, &block) ⇒ Object Originally defined in module 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.
#sample(now = ::Time.now) ⇒ Object
One sampling pass, bypassing the leader gate and the sleep loop. Public so deterministic specs and a manual "sample now" can drive it directly.
85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 |
# File 'lib/wurk/metrics/queue_rollup.rb', line 85 def sample(now = ::Time.now) gauges = queue_gauges return if gauges.empty? fields = gauges.flat_map do |name, (size, lat)| ["#{name}|#{SIZE_KIND}", size, "#{name}|#{LAT_KIND}", lat] end BUCKETS.each do |bucket, (step, ttl)| key = self.class.bucket_key(bucket, (now.to_i / step) * step) redis do |c| c.call('HSET', key, *fields) c.call('EXPIRE', key, ttl) end end nil end |
#start ⇒ Object
53 54 55 56 57 58 |
# File 'lib/wurk/metrics/queue_rollup.rb', line 53 def start return @thread if @thread @timer.reset @thread = safe_thread('queue-metrics') { @timer.run { tick } } end |
#terminate ⇒ Object
Blocks until the thread is really gone: the launcher releases the cluster lock immediately after this returns, and a sample still in flight would write the same buckets as the next leader's first one.
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 calling @timer.reset, which would un-terminate the loop it is still inside and leave two threads HSETting the same buckets.
68 69 70 71 |
# File 'lib/wurk/metrics/queue_rollup.rb', line 68 def terminate @timer.terminate @thread = nil if @thread&.join(TimerLoop::JOIN_TIMEOUT) end |
#tick(now: ::Time.now) ⇒ Object
Leader-gated: only the elected leader samples, so N workers don't each HSET the same buckets every minute.
75 76 77 78 79 80 81 |
# File 'lib/wurk/metrics/queue_rollup.rb', line 75 def tick(now: ::Time.now) return unless leader? sample(now) rescue StandardError => e handle_exception(e, { context: 'queue-metrics' }) end |
#tid ⇒ Object Originally defined in module Component
#watchdog(last_words) ⇒ Object Originally defined in module 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.