Class: Wurk::Heartbeat
- Inherits:
-
Object
- Object
- Wurk::Heartbeat
- Includes:
- Component
- Defined in:
- lib/wurk/heartbeat.rb
Overview
Single-purpose owner of the Redis-side process heartbeat. Lifted out of Wurk::Launcher so the launcher can stay focused on lifecycle and so the heartbeat schema lives in one place readers can grep for.
Each beat is two pipelined round-trips. The identity write:
SADD processes <identity>
HSET <identity> info concurrency busy beat quiet rss rtt_us
EXPIRE <identity> 60
UNLINK <identity>:work
HSET <identity>:work <tid> <json> ... (only if WORK_STATE non-empty)
EXPIRE <identity>:work 60 (only if WORK_STATE non-empty)
EVALSHA refresh_slots <held slots> (only if QueueSlot::HELD non-empty)
then the signal drain:
RPOP <identity>-signals BEAT_PAUSE
Two rather than one because only the first is safe to replay — see #pipelined_beat and #drain_signals.
The work hash is UNLINK-then-rewritten on every beat — a dropped beat
momentarily empties it, and ProcessSet#cleanup compensates by SREM-ing
identities whose info field has expired.
Heartbeat owns only the wire. Signals queued by the dashboard are pulled out of the pipelined results and returned to the caller for dispatch — TSTP/TERM semantics live in Launcher.
:heartbeat fires once on the first successful beat and again after any
network partition (rearmed when beat! rescues). :beat fires every tick.
Spec: docs/target/sidekiq-free.md §12 (Launcher#❤️).
Constant Summary collapse
- BEAT_PAUSE =
Cadence in seconds. Key TTL is 60s — a process is dead after ~6 misses.
10- TTL_SECONDS =
60- OOM_REFRESH_SLOTS =
"#{Fetcher::OomDrain::HEADER}#{Lua::SCRIPTS.fetch(:refresh_slots)}".freeze
- OOM_BEAT =
KEYS = [processes, identity, work], ARGV = [info_json, beat, ttl_seconds, tid, work_json, ...]
<<~LUA.freeze #{Fetcher::OomDrain::HEADER}if redis.call('EXISTS', KEYS[2]) == 0 then redis.call('HSET', KEYS[2], 'info', ARGV[1]) redis.call('SADD', KEYS[1], KEYS[2]) end redis.call('HSET', KEYS[2], 'beat', ARGV[2]) redis.call('EXPIRE', KEYS[2], ARGV[3]) redis.call('UNLINK', KEYS[3]) for i = 4, #ARGV, 2 do redis.call('HSET', KEYS[3], ARGV[i], ARGV[i + 1]) end redis.call('EXPIRE', KEYS[3], ARGV[3]) return 1 LUA
- FALLBACK_PAGE_SIZE =
4096- PAGE_SIZE =
page_size
Instance Attribute Summary collapse
-
#config ⇒ Object
included
from Component
readonly
Returns the value of attribute config.
-
#identity ⇒ Object
readonly
Returns the value of attribute identity.
-
#last_beat_at ⇒ Object
readonly
Returns the value of attribute last_beat_at.
-
#rtt_us ⇒ Object
readonly
Returns the value of attribute rtt_us.
Class Method Summary collapse
-
.page_size(etc = Etc) ⇒ Object
statm counts pages, and a page is not 4 KB everywhere: arm64 kernels commonly run 16 KB or 64 KB pages, where a hard-coded ×4 under-reports RSS by 4–16×.
-
.smaps_kb(pid, fields, proc_root: '/proc') ⇒ Object
Sum of
fields(kB) in /proc//smaps_rollup; nil when none is there. -
.statm_rss_kb(statm) ⇒ Object
Resident KB from a /proc/
/statm line (field 1 is resident pages).
Instance Method Summary collapse
-
#beat! ⇒ Object
Pipelined beat.
- #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
-
#initialize(identity:, config:, started_at: nil, embedded: false, quiet: nil) ⇒ Heartbeat
constructor
quiet:is a callable so Launcher can keep ownership of its@doneflag without an awkward setter contract. -
#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). -
#stop! ⇒ Object
Best-effort teardown.
- #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(identity:, config:, started_at: nil, embedded: false, quiet: nil) ⇒ Heartbeat
quiet: is a callable so Launcher can keep ownership of its @done
flag without an awkward setter contract.
103 104 105 106 107 108 109 110 111 |
# File 'lib/wurk/heartbeat.rb', line 103 def initialize(identity:, config:, started_at: nil, embedded: false, quiet: nil) @identity = identity @config = config @started_at = started_at || Time.now.to_f @embedded = @quiet_proc = quiet || -> { false } @first_heartbeat = true @rtt_us = 0 end |
Instance Attribute Details
#config ⇒ Object (readonly) Originally defined in module Component
Returns the value of attribute config.
#identity ⇒ Object (readonly)
Returns the value of attribute identity.
99 100 101 |
# File 'lib/wurk/heartbeat.rb', line 99 def identity @identity end |
#last_beat_at ⇒ Object (readonly)
Returns the value of attribute last_beat_at.
99 100 101 |
# File 'lib/wurk/heartbeat.rb', line 99 def last_beat_at @last_beat_at end |
#rtt_us ⇒ Object (readonly)
Returns the value of attribute rtt_us.
99 100 101 |
# File 'lib/wurk/heartbeat.rb', line 99 def rtt_us @rtt_us end |
Class Method Details
.page_size(etc = Etc) ⇒ Object
statm counts pages, and a page is not 4 KB everywhere: arm64 kernels commonly run 16 KB or 64 KB pages, where a hard-coded ×4 under-reports RSS by 4–16×.
71 72 73 74 75 76 77 |
# File 'lib/wurk/heartbeat.rb', line 71 def self.page_size(etc = Etc) size = etc.sysconf(Etc::SC_PAGESIZE) size.is_a?(Integer) && size.positive? ? size : FALLBACK_PAGE_SIZE # NotImplementedError (a ScriptError) is what Etc raises without sysconf. rescue StandardError, NotImplementedError FALLBACK_PAGE_SIZE end |
.smaps_kb(pid, fields, proc_root: '/proc') ⇒ Object
Sum of fields (kB) in /proc/
90 91 92 93 94 95 96 97 |
# File 'lib/wurk/heartbeat.rb', line 90 def self.smaps_kb(pid, fields, proc_root: '/proc') total = nil ::File.foreach("#{proc_root}/#{pid}/smaps_rollup") do |line| name, value = line.split(':', 2) total = total.to_i + value.to_i if value && fields.include?(name) end total end |
.statm_rss_kb(statm) ⇒ Object
Resident KB from a /proc/
83 84 85 |
# File 'lib/wurk/heartbeat.rb', line 83 def self.statm_rss_kb(statm) statm.split[1].to_i * PAGE_SIZE / 1024 end |
Instance Method Details
#beat! ⇒ Object
Pipelined beat. Returns the drained signals as Array:heartbeat so partition recovery is observable.
116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 |
# File 'lib/wurk/heartbeat.rb', line 116 def beat! sigs, @rtt_us = pipelined_beat @last_beat_at = Time.now.to_f if @first_heartbeat @first_heartbeat = false fire_event(:heartbeat) end fire_event(:beat, oneshot: false) emit_statsd_gauges sigs rescue StandardError => e @first_heartbeat = true handle_exception(e, { context: 'heartbeat' }) nil end |
#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
#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.
#stop! ⇒ Object
Best-effort teardown. SREM removes us from the live list; UNLINK drops the identity hash and its work mirror. Idempotent: if Redis is down the next process boot's ProcessSet#cleanup prunes us.
135 136 137 138 139 140 141 142 143 144 |
# File 'lib/wurk/heartbeat.rb', line 135 def stop! redis do |conn| conn.pipelined do |pipe| pipe.call('SREM', Keys::PROCESSES, @identity) pipe.call('UNLINK', @identity, "#{@identity}:work") end end rescue StandardError nil 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.