Class: Wurk::CLI

Inherits:
Object
  • Object
show all
Includes:
Component
Defined in:
lib/wurk/cli.rb

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
}.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_i parses as Integer, :append pushes onto a list (only -q uses 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

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initializeCLI

Returns a new instance of CLI.



77
78
79
80
81
82
# File 'lib/wurk/cli.rb', line 77

def initialize
  @config = nil
  @launcher = nil
  @environment = nil
  @parser = nil
end

Instance Attribute Details

#configObject

Returns the value of attribute config.



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

def config
  @config
end

#configObject (readonly) Originally defined in module Component

Returns the value of attribute config.

#environmentObject

Returns the value of attribute environment.



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

def environment
  @environment
end

#launcherObject

Returns the value of attribute launcher.



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

def launcher
  @launcher
end

Class Method Details

.instanceObject



68
69
70
# File 'lib/wurk/cli.rb', line 68

def self.instance
  @instance ||= new
end

.reset_instance!Object

Test seam: parallel suites can't share the singleton.



73
74
75
# File 'lib/wurk/cli.rb', line 73

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



145
146
147
148
149
150
151
# File 'lib/wurk/cli.rb', line 145

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

#hostnameObject Originally defined in module Component

#identityObject Originally defined in module Component

#leader?Boolean Originally defined in module Component

True iff this process currently holds the cluster dear-leader lock. Per spec, the check is performed at call time (Wurk does not cache); callers must not poll faster than the 60s follower cadence. 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)

#loggerObject Originally defined in module Component

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

#mono_msObject 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.



86
87
88
89
90
91
92
# File 'lib/wurk/cli.rb', line 86

def parse(args = ARGV.dup)
  @config ||= Wurk.default_configuration
  setup_options(args)
  initialize_logger
  validate!
  self
end

#process_nonceObject Originally defined in module Component

#real_msObject Originally defined in module Component

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

#redisObject Originally defined in module Component

#run(boot_app: true, warmup: true) ⇒ Object

boot_app: / warmup: are test seams. Production always passes true.



95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
# File 'lib/wurk/cli.rb', line 95

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
  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
  ::Process.warmup if warmup && ::Process.respond_to?(:warmup) && ENV['RUBY_DISABLE_WARMUP'] != '1'
  fire_event(:startup, reverse: false, reraise: true)
  launch(self_read)
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.



126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
# File 'lib/wurk/cli.rb', line 126

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
  ::Process.warmup if warmup && ::Process.respond_to?(:warmup) && ENV['RUBY_DISABLE_WARMUP'] != '1'
  @swarm = Wurk::Swarm.new(topology: @config.topology, config: @config)
  @swarm.boot(install_signals: true)
  @swarm.supervise
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.

#tidObject Originally defined in module Component

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

#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.