Class: Wurk::CLI
Overview
Standalone CLI. Loads from exe/wurk — never loads wurk/rails so the
binary works without the Rails engine (the host app might not be Rails).
Singleton because there is exactly one process-wide CLI; tests construct
fresh .new instances to keep state isolated.
Spec: docs/target/sidekiq-free.md §21 (Sidekiq::CLI).
Constant Summary collapse
- MIN_REDIS_VERSION =
Minimum Redis version Wurk supports — same as Sidekiq 8.x. The job JSON format and Lua scripts rely on commands introduced in Redis 7.
'7.0.0'- BACKTRACE_DUMPER =
Thread-backtrace dumper used by both TTIN and INFO. Same body — INFO is the modern name, TTIN is kept for parity with older Sidekiq users.
lambda do |cli| Thread.list.each do |thread| cli.logger.warn "Thread TID-#{(thread.object_id ^ ::Process.pid).to_s(36)} #{thread.name}" if thread.backtrace cli.logger.warn thread.backtrace.join("\n") else cli.logger.warn '<no backtrace available>' end end end
- SIGNAL_HANDLERS =
{ 'INT' => ->(_cli) { raise Interrupt }, 'TERM' => ->(_cli) { raise Interrupt }, 'TSTP' => lambda do |cli| cli.logger.info 'Received TSTP, no longer accepting new work' cli.launcher.quiet end, 'TTIN' => BACKTRACE_DUMPER, 'INFO' => BACKTRACE_DUMPER, 'USR2' => lambda do |cli| cli.logger.info 'Received USR2, reopening logs' # reopen_logs is private — an explicit receiver needs __send__. cli.__send__(:reopen_logs) end }.freeze
- OPTION_FLAGS =
Table-driven so adding a flag doesn't grow
define_value_flags's ABC size and the surface matches the Sidekiq docs row-for-row. The 5th column is the assignment transform::to_iparses as Integer,:appendpushes onto a list (only-quses that), nil = assign as-is. [ ['-c', '--concurrency INT', :concurrency, 'processor threads to use', :to_i], ['-e', '--environment ENV', :environment, 'Application environment'], ['-g', '--tag TAG', :tag, 'Process tag for procline'], ['-q', '--queue QUEUE[,WEIGHT]', :queues, 'Queues to process with optional weights', :append], ['-r', '--require [PATH|DIR]', :require, 'Location of Rails app or .rb file to require'], ['-t', '--timeout NUM', :timeout, 'Shutdown timeout', :to_i], ['-v', '--verbose', :verbose, 'Print more verbose output'], ['-C', '--config PATH', :config_file, 'path to YAML config file'] ].freeze
Instance Attribute Summary collapse
-
#config ⇒ Object
Returns the value of attribute config.
-
#config ⇒ Object
included
from Component
readonly
Returns the value of attribute config.
-
#environment ⇒ Object
Returns the value of attribute environment.
-
#launcher ⇒ Object
Returns the value of attribute launcher.
Class Method Summary collapse
- .instance ⇒ Object
-
.reset_instance! ⇒ Object
Test seam: parallel suites can't share the singleton.
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
- #handle_signal(sig) ⇒ Object
- #hostname ⇒ Object included from Component
- #identity ⇒ Object included from Component
-
#initialize ⇒ CLI
constructor
A new instance of CLI.
-
#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
-
#parse(args = ARGV.dup) ⇒ Object
parseis split fromrunso tests can drive option parsing without touching Redis or booting the host app. - #process_nonce ⇒ Object included from Component
-
#real_ms ⇒ Object
included
from Component
--- clocks ---------------------------------------------------------.
- #redis(idempotent: false) ⇒ Object included from Component
-
#run(boot_app: true, warmup: true) ⇒ Object
boot_app:/warmup:are test seams. -
#run_swarm(boot_app: true, warmup: true) ⇒ Object
Standalone multi-process boot — the
sidekiqswarmentry point (Ent §7). -
#safe_thread(name, priority: nil, &block) ⇒ Object
included
from Component
Spawns a named thread that runs
blockunderwatchdog(name). - #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 ⇒ CLI
Returns a new instance of CLI.
82 83 84 85 86 87 |
# File 'lib/wurk/cli.rb', line 82 def initialize @config = nil @launcher = nil @environment = nil @parser = nil end |
Instance Attribute Details
#config ⇒ Object
Returns the value of attribute config.
71 72 73 |
# File 'lib/wurk/cli.rb', line 71 def config @config end |
#config ⇒ Object (readonly) Originally defined in module Component
Returns the value of attribute config.
#environment ⇒ Object
Returns the value of attribute environment.
71 72 73 |
# File 'lib/wurk/cli.rb', line 71 def environment @environment end |
#launcher ⇒ Object
Returns the value of attribute launcher.
71 72 73 |
# File 'lib/wurk/cli.rb', line 71 def launcher @launcher end |
Class Method Details
.instance ⇒ Object
73 74 75 |
# File 'lib/wurk/cli.rb', line 73 def self.instance @instance ||= new end |
.reset_instance! ⇒ Object
Test seam: parallel suites can't share the singleton.
78 79 80 |
# File 'lib/wurk/cli.rb', line 78 def self.reset_instance! @instance = nil 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
#handle_signal(sig) ⇒ Object
166 167 168 169 170 171 172 |
# File 'lib/wurk/cli.rb', line 166 def handle_signal(sig) logger.debug { "Got #{sig} signal" } handler = SIGNAL_HANDLERS[sig] return logger.warn("No #{sig} signal handler registered, ignoring") unless handler handler.call(self) end |
#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
#parse(args = ARGV.dup) ⇒ Object
parse is split from run so tests can drive option parsing without
touching Redis or booting the host app.
91 92 93 94 95 96 97 |
# File 'lib/wurk/cli.rb', line 91 def parse(args = ARGV.dup) @config ||= Wurk.default_configuration (args) initialize_logger validate! self end |
#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
#run(boot_app: true, warmup: true) ⇒ Object
boot_app: / warmup: are test seams. Production always passes true.
100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 |
# File 'lib/wurk/cli.rb', line 100 def run(boot_app: true, warmup: true) # Mark server mode BEFORE the app loads so `configure_server` blocks in # the required initializer actually fire (they gate on `config.server?`). # Matches Sidekiq, which sets `Sidekiq.server = true` before requiring # the app in its CLI. Skipping this silently drops server middleware, # error handlers, and lifecycle hooks registered via configure_server. enter_server_mode boot_application if boot_app self_read, self_write = ::IO.pipe begin trap_signals(self_write) validate_redis! validate_pool_sizes! @config[:identity] = identity # Force lazy server-middleware chain so worker threads don't race # against each other constructing it. Spec: Sidekiq::CLI line 104. @config.server_middleware warm_up_process if warmup fire_event(:startup, reverse: false, reraise: true) launch(self_read) ensure # Every exit path — clean return, a raise out of validate/startup, and # the SystemExit that `launch` unwinds on Interrupt. Embedded and test # callers survive `run`, so a leaked pair is a real FD leak. self_read.close self_write.close end end |
#run_swarm(boot_app: true, warmup: true) ⇒ Object
Standalone multi-process boot — the sidekiqswarm entry point (Ent §7).
The parent loads the app once, forks N worker children per the configured
topology, then supervises them (respawn on crash, rolling restart on
SIGUSR1, memory-based recycling). The parent itself never fetches.
This is the only way to get fork-based parallelism without Rails — the
railtie auto-boot path is Rails-only. Honors the swarm preload knobs
(WURK_PRELOAD/SIDEKIQ_PRELOAD Bundler groups, WURK_PRELOAD_APP/
SIDEKIQ_PRELOAD_APP whole-app eager-load) and boots Process.warmup
before the fork so children share warmed pages (copy-on-write). Spec §7.
139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 |
# File 'lib/wurk/cli.rb', line 139 def run_swarm(boot_app: true, warmup: true) # Server mode before the app loads — see #run. The flag rides through the # fork into every child (the config object is copied), so configure_server # blocks registered in the parent take effect in the workers. enter_server_mode if boot_app preload_bundler_groups boot_application eager_load_application end validate_redis! validate_pool_sizes! @config[:identity] = identity warm_up_process if warmup @swarm = Wurk::Swarm.new(topology: @config.topology, config: @config, shutdown_timeout: @config[:timeout] || Swarm::DEFAULT_SHUTDOWN_TIMEOUT) begin @swarm.boot(install_signals: true) @swarm.supervise ensure # A fork failure part-way through `boot`, or anything raising out of # `supervise`, otherwise leaves live children behind with no supervisor. # A no-op once the loop already drained (no children, pipe closed). @swarm.shutdown end end |
#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.
#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.