Module: Wurk::Component

Overview

Shared mixin for runtime components (Launcher, Manager, Processor, Fetcher, Scheduler, Cron). Wraps clock readings, identity, thread spawning, lifecycle event dispatch, and exception forwarding so each component stays single-purpose.

Host class must expose #config returning either a Wurk::Configuration or a Wurk::Capsule — both duck-type the methods we delegate to.

Spec: docs/target/sidekiq-free.md §11 (Sidekiq::Component).

Constant Summary collapse

DEFAULT_THREAD_PRIORITY =
-1
PROCESS_NONCE =

Stable for the life of the process — survives fork (children inherit the same nonce). Identity differs across forks because Process.pid does.

SecureRandom.hex(6)
LEADER_CACHE_TTL_MS =

leader? cache TTL — see the method doc below.

5_000

Instance Attribute Summary collapse

Instance Method Summary collapse

Instance Attribute Details

#configObject (readonly)

Returns the value of attribute config.



23
24
25
# File 'lib/wurk/component.rb', line 23

def config
  @config
end

Instance Method Details

#default_tag(dir = Dir.pwd) ⇒ Object



56
57
58
# File 'lib/wurk/component.rb', line 56

def default_tag(dir = Dir.pwd)
  File.basename(dir)
end

#fire_event(event, oneshot: true, reverse: false, reraise: false) ⇒ Object

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.



127
128
129
130
131
132
133
134
# File 'lib/wurk/component.rb', line 127

def fire_event(event, oneshot: true, reverse: false, reraise: false)
  bucket = config[:lifecycle_events][event]
  return if bucket.nil? || bucket.empty?

  iter = reverse ? bucket.reverse : bucket
  iter.each { |hook| run_lifecycle_hook(hook, event, reraise) }
  bucket.clear if oneshot
end

#handle_exception(ex, ctx = {}) ⇒ Object



70
71
72
# File 'lib/wurk/component.rb', line 70

def handle_exception(ex, ctx = {})
  config.handle_exception(ex, ctx)
end

#hostnameObject



44
45
46
# File 'lib/wurk/component.rb', line 44

def hostname
  ENV['DYNO'] || Socket.gethostname
end

#identityObject



52
53
54
# File 'lib/wurk/component.rb', line 52

def identity
  "#{hostname}:#{::Process.pid}:#{process_nonce}"
end

#leader?Boolean

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.

Returns:

  • (Boolean)


87
88
89
90
91
92
93
94
95
96
# File 'lib/wurk/component.rb', line 87

def leader?
  return false if Wurk::Leader.opted_out?

  now = mono_ms
  if @leader_checked_at.nil? || (now - @leader_checked_at) >= LEADER_CACHE_TTL_MS
    @leader_checked_at = now
    @leader_cached = fetch_leader?
  end
  @leader_cached
end

#loggerObject

--- delegated to config -------------------------------------------



62
63
64
# File 'lib/wurk/component.rb', line 62

def logger
  config.logger
end

#mono_msObject



34
35
36
# File 'lib/wurk/component.rb', line 34

def mono_ms
  ::Process.clock_gettime(::Process::CLOCK_MONOTONIC, :millisecond)
end

#process_nonceObject



48
49
50
# File 'lib/wurk/component.rb', line 48

def process_nonce
  PROCESS_NONCE
end

#real_msObject

--- clocks ---------------------------------------------------------



30
31
32
# File 'lib/wurk/component.rb', line 30

def real_ms
  ::Process.clock_gettime(::Process::CLOCK_REALTIME, :millisecond)
end

#redisObject



66
67
68
# File 'lib/wurk/component.rb', line 66

def redis(&)
  config.redis(&)
end

#safe_thread(name, priority: nil, &block) ⇒ Object

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.



113
114
115
116
117
118
119
120
# File 'lib/wurk/component.rb', line 113

def safe_thread(name, priority: nil, &block)
  Thread.new do
    Thread.current.name = name
    Thread.current.priority = priority || DEFAULT_THREAD_PRIORITY
    Thread.current.report_on_exception = false
    watchdog(name, &block)
  end
end

#tidObject

--- identity -------------------------------------------------------



40
41
42
# File 'lib/wurk/component.rb', line 40

def tid
  (Thread.current.object_id ^ ::Process.pid).to_s(36)
end

#watchdog(last_words) ⇒ Object

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.



103
104
105
106
107
108
# File 'lib/wurk/component.rb', line 103

def watchdog(last_words)
  yield
rescue StandardError => e
  handle_exception(e, { context: last_words })
  raise
end