wurk 1.9.0 → 1.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +9 -9
- data/app/controllers/wurk/api_controller.rb +4 -1
- data/app/controllers/wurk/dashboard_controller.rb +11 -6
- data/exe/wurk +7 -2
- data/exe/wurkswarm +1 -2
- data/lib/generators/wurk/install/install_generator.rb +12 -0
- data/lib/sidekiq/web.rb +5 -0
- data/lib/wurk/boot_checks.rb +93 -0
- data/lib/wurk/capsule.rb +33 -8
- data/lib/wurk/cli.rb +204 -30
- data/lib/wurk/client/buffered.rb +240 -26
- data/lib/wurk/client.rb +16 -5
- data/lib/wurk/compat.rb +15 -0
- data/lib/wurk/configuration.rb +114 -16
- data/lib/wurk/cron.rb +141 -16
- data/lib/wurk/debounce.rb +2 -2
- data/lib/wurk/embedded.rb +8 -2
- data/lib/wurk/encryption.rb +20 -5
- data/lib/wurk/error_coalescer.rb +35 -0
- data/lib/wurk/fetcher/at_maxmemory.rb +79 -0
- data/lib/wurk/fetcher/capped.rb +4 -0
- data/lib/wurk/fetcher/many_queues.rb +17 -1
- data/lib/wurk/fetcher/oom_drain.rb +102 -0
- data/lib/wurk/fetcher/orphan_grace.rb +60 -44
- data/lib/wurk/fetcher/private_list_key.rb +1 -16
- data/lib/wurk/fetcher/reaper.rb +169 -127
- data/lib/wurk/fetcher/reliable.rb +26 -4
- data/lib/wurk/fetcher/self_reconcile.rb +175 -0
- data/lib/wurk/fetcher/unit_of_work.rb +32 -8
- data/lib/wurk/health.rb +23 -4
- data/lib/wurk/health_probe.rb +64 -0
- data/lib/wurk/heartbeat.rb +55 -3
- data/lib/wurk/job/options.rb +9 -1
- data/lib/wurk/job.rb +2 -24
- data/lib/wurk/job_record.rb +17 -9
- data/lib/wurk/job_retry.rb +38 -10
- data/lib/wurk/job_set.rb +13 -5
- data/lib/wurk/launcher.rb +134 -52
- data/lib/wurk/leader.rb +71 -11
- data/lib/wurk/limiter/base.rb +17 -1
- data/lib/wurk/lua/debounce.lua +11 -6
- data/lib/wurk/lua/loader.rb +13 -1
- data/lib/wurk/lua.rb +71 -8
- data/lib/wurk/manager.rb +1 -1
- data/lib/wurk/metrics/process_counters.rb +27 -0
- data/lib/wurk/metrics/prometheus.rb +35 -14
- data/lib/wurk/middleware/current_attributes.rb +26 -10
- data/lib/wurk/middleware/poison_pill.rb +1 -2
- data/lib/wurk/preflight/checks.rb +203 -0
- data/lib/wurk/preflight/command.rb +135 -0
- data/lib/wurk/preflight/keyspace.rb +109 -0
- data/lib/wurk/preflight/owners.rb +74 -0
- data/lib/wurk/preflight/payload_sample.rb +108 -0
- data/lib/wurk/preflight/reader.rb +60 -0
- data/lib/wurk/preflight/report.rb +82 -0
- data/lib/wurk/preflight/verify.rb +161 -0
- data/lib/wurk/preflight.rb +40 -0
- data/lib/wurk/process_set.rb +3 -1
- data/lib/wurk/processor.rb +82 -18
- data/lib/wurk/queue_consumers.rb +134 -0
- data/lib/wurk/rails_boot.rb +40 -2
- data/lib/wurk/rails_reloader.rb +51 -0
- data/lib/wurk/railtie.rb +20 -1
- data/lib/wurk/redact.rb +12 -0
- data/lib/wurk/redis_pool.rb +34 -9
- data/lib/wurk/scheduled.rb +142 -67
- data/lib/wurk/sentry/death_handler.rb +29 -0
- data/lib/wurk/sentry/error_handler.rb +52 -10
- data/lib/wurk/sentry/middleware.rb +8 -1
- data/lib/wurk/sentry/retry_policy.rb +12 -3
- data/lib/wurk/sentry.rb +42 -5
- data/lib/wurk/sorted_entry.rb +38 -13
- data/lib/wurk/swarm/child_boot.rb +44 -31
- data/lib/wurk/swarm/crash_watch.rb +65 -0
- data/lib/wurk/swarm/test_mode_guard.rb +32 -0
- data/lib/wurk/swarm.rb +79 -11
- data/lib/wurk/testing.rb +74 -0
- data/lib/wurk/version.rb +1 -1
- data/lib/wurk/web/config.rb +8 -4
- data/lib/wurk/web/extension.rb +41 -62
- data/lib/wurk/web/helpers.rb +133 -0
- data/lib/wurk/web/paginator.rb +49 -0
- data/lib/wurk/web/rack_app.rb +30 -6
- data/lib/wurk/work_set.rb +6 -0
- data/lib/wurk/worker/setter.rb +11 -6
- data/lib/wurk/worker.rb +17 -1
- data/lib/wurk.rb +1 -1
- data/vendor/assets/dashboard/assets/{ArgsValue-DDqX6XWH.js → ArgsValue-CmaXArv8.js} +1 -1
- data/vendor/assets/dashboard/assets/{BatchDetail-lIytcn2C.js → BatchDetail-nc5yePZW.js} +1 -1
- data/vendor/assets/dashboard/assets/{Batches-BEumt-1O.js → Batches-DgEcyOZ_.js} +1 -1
- data/vendor/assets/dashboard/assets/{Busy-Vk9HmZcN.js → Busy-CUZncvCy.js} +1 -1
- data/vendor/assets/dashboard/assets/{Cron-CWcM-Cfr.js → Cron-CIZbaaZs.js} +1 -1
- data/vendor/assets/dashboard/assets/{Dashboard-Bgv1kusv.js → Dashboard-BWmfAook.js} +1 -1
- data/vendor/assets/dashboard/assets/{Dead-Di5Rct2H.js → Dead-CGKsTZHq.js} +1 -1
- data/vendor/assets/dashboard/assets/{Extension-LZY5-imc.js → Extension-Doy4bJoA.js} +1 -1
- data/vendor/assets/dashboard/assets/{FilterBox-BDo-7IAy.js → FilterBox-C4bO26_d.js} +1 -1
- data/vendor/assets/dashboard/assets/{FlowDetail-CehwRLTt.js → FlowDetail-Dh3SJdFh.js} +1 -1
- data/vendor/assets/dashboard/assets/{FlowState-glK2Tnf8.js → FlowState-yb7virqR.js} +1 -1
- data/vendor/assets/dashboard/assets/{Flows-cgy-Fsbp.js → Flows-C5SMRuhq.js} +1 -1
- data/vendor/assets/dashboard/assets/{JobDetailModal-Cuk3jruU.js → JobDetailModal-Cg6sAsJB.js} +1 -1
- data/vendor/assets/dashboard/assets/{Limiters-Ci8G2yZG.js → Limiters-BIxwDOJM.js} +1 -1
- data/vendor/assets/dashboard/assets/{Metrics-CQFUeZTR.js → Metrics-Qp9q0nF8.js} +1 -1
- data/vendor/assets/dashboard/assets/{NotFound-CYjhuDsI.js → NotFound-DwjPxFjU.js} +1 -1
- data/vendor/assets/dashboard/assets/{PageHeader-DjIAFTyX.js → PageHeader-vWeBbSPF.js} +1 -1
- data/vendor/assets/dashboard/assets/{Profiles-DTowqIRg.js → Profiles-7dabYVe3.js} +1 -1
- data/vendor/assets/dashboard/assets/Queues-CMg-gyP8.js +1 -0
- data/vendor/assets/dashboard/assets/{Retries-SJ3BaKC0.js → Retries-CNX8ZtTH.js} +1 -1
- data/vendor/assets/dashboard/assets/{Scheduled-DxSRpmFZ.js → Scheduled-ClVBj0nc.js} +1 -1
- data/vendor/assets/dashboard/assets/{Search-bBbi5JNK.js → Search-BAsVE2qw.js} +1 -1
- data/vendor/assets/dashboard/assets/{charts-Bg2YBcaD.js → charts-DZs138BV.js} +1 -1
- data/vendor/assets/dashboard/assets/i18n-CBewVzQ_.js +1 -0
- data/vendor/assets/dashboard/assets/index--xvvNlYx.css +1 -0
- data/vendor/assets/dashboard/assets/{index-BOTVSP9M.js → index-CMzPFxNE.js} +3 -3
- data/vendor/assets/dashboard/assets/{useResetPageOnEmpty-BRPNyV2M.js → useResetPageOnEmpty-DxkLZCIm.js} +1 -1
- data/vendor/assets/dashboard/assets/{useSort-B-BLJik7.js → useSort-jBYVwXvg.js} +1 -1
- data/vendor/assets/dashboard/assets/{utils-CGyrKpTh.js → utils-B0kQFNE8.js} +1 -1
- data/vendor/assets/dashboard/index.html +3 -3
- data/vendor/assets/dashboard/wurk-manifest.json +2 -2
- metadata +53 -30
- data/vendor/assets/dashboard/assets/Queues-DUGRPjME.js +0 -1
- data/vendor/assets/dashboard/assets/i18n-BZClP62Q.js +0 -1
- data/vendor/assets/dashboard/assets/index-Rewk4eSE.css +0 -1
data/lib/wurk/cli.rb
CHANGED
|
@@ -6,11 +6,14 @@
|
|
|
6
6
|
# a Rails app that enqueues and processes jobs pays that on every boot for a
|
|
7
7
|
# command-line parser it never runs. `require` is idempotent, so the lazy call
|
|
8
8
|
# is a $LOADED_FEATURES hash lookup on every invocation after the first.
|
|
9
|
+
require 'rbconfig'
|
|
9
10
|
require_relative 'version'
|
|
10
11
|
require_relative 'configuration'
|
|
11
12
|
require_relative 'component'
|
|
12
13
|
require_relative 'capsule'
|
|
13
14
|
require_relative 'launcher'
|
|
15
|
+
require_relative 'rails_boot'
|
|
16
|
+
require_relative 'boot_checks'
|
|
14
17
|
|
|
15
18
|
module Wurk
|
|
16
19
|
# Standalone CLI. Loads from `exe/wurk` — never loads `wurk/rails` so the
|
|
@@ -29,7 +32,15 @@ module Wurk
|
|
|
29
32
|
# Subcommands. Bare `wurk` runs the worker — the historical shape, and the
|
|
30
33
|
# only one the swarm binaries accept — so a subcommand is never anything
|
|
31
34
|
# but the first argument, and every argument after it is that command's.
|
|
32
|
-
COMMANDS = %w[api].freeze
|
|
35
|
+
COMMANDS = %w[api health].freeze
|
|
36
|
+
# Set only across the one re-exec of #load_rails_framework, and removed on arrival.
|
|
37
|
+
PRELOAD_FIRST_ENV = 'WURK_BOOT_PRELOAD_FIRST'
|
|
38
|
+
# `bundle exec` loads the binstub in-process, so argv0 is Bundler's executable, and
|
|
39
|
+
# `ruby -e "load Gem.bin_path(...)"` has `-e` there: the running program can't name it.
|
|
40
|
+
SWARM_EXECUTABLE = ::File.expand_path('../../exe/wurkswarm', __dir__)
|
|
41
|
+
|
|
42
|
+
# A refusal this file raises itself, with nothing a trace would add.
|
|
43
|
+
class StartupError < ArgumentError; end
|
|
33
44
|
|
|
34
45
|
# `wurk api` defaults. 0.0.0.0 because the point of the machine API is for
|
|
35
46
|
# another service to reach it; it is bearer-gated, and answers 404 to
|
|
@@ -99,6 +110,23 @@ module Wurk
|
|
|
99
110
|
['-s', '--server NAME', :api_server, 'Rack handler to serve with (default: the first installed)']
|
|
100
111
|
].freeze
|
|
101
112
|
|
|
113
|
+
HEALTH_OPTION_FLAGS = [
|
|
114
|
+
['-w', '--window SECONDS', :health_window, 'A heartbeat older than this does not count (default 60)', :to_i],
|
|
115
|
+
['-m', '--min COUNT', :health_min, 'Processes on this host that must be beating (default 1)', :to_i],
|
|
116
|
+
['-u', '--redis-url URL', :health_redis_url, 'Redis to read (default REDIS_URL)'],
|
|
117
|
+
['-s', '--strict', :health_strict, 'Fail when Redis cannot be read, rather than passing']
|
|
118
|
+
].freeze
|
|
119
|
+
|
|
120
|
+
HELP_COMMANDS = "\nCommands:\n " \
|
|
121
|
+
"health Exit 0 if this host's workers are heartbeating\n " \
|
|
122
|
+
"preflight Read a live Redis: what will carry over to Wurk (read-only)\n " \
|
|
123
|
+
"verify Post-cutover check of a live fleet (read-only)\n\nOptions:"
|
|
124
|
+
HELP_API_NOTE = "\nThe worker flags -c, -g, -q and -t are accepted here and have no effect.\n\nOptions:"
|
|
125
|
+
HELP_SWARM_NOTE = "\nForks WURK_COUNT worker processes from a preloaded parent and supervises them.\n\nOptions:"
|
|
126
|
+
HELP_FOOTER = "\nEnvironment:\n REDIS_URL Redis to use (default redis://localhost:6379/0)\n " \
|
|
127
|
+
"WURK_COUNT Worker processes for wurkswarm\n\n" \
|
|
128
|
+
'Docs: https://github.com/developerz-ai/wurk#readme'
|
|
129
|
+
|
|
102
130
|
attr_accessor :launcher, :environment, :config
|
|
103
131
|
|
|
104
132
|
# The subcommand `parse` pulled off argv, or nil for the worker runner.
|
|
@@ -127,14 +155,44 @@ module Wurk
|
|
|
127
155
|
def parse(args = ARGV.dup)
|
|
128
156
|
@config ||= Wurk.default_configuration
|
|
129
157
|
@command = extract_command!(args)
|
|
158
|
+
return parse_health_options(args) if @command == 'health'
|
|
159
|
+
|
|
130
160
|
setup_options(args)
|
|
131
161
|
initialize_logger
|
|
132
162
|
validate!
|
|
133
163
|
self
|
|
134
164
|
end
|
|
135
165
|
|
|
166
|
+
# What the binaries print for a failed start. Only errors this file fully
|
|
167
|
+
# understands drop the trace. Anything else may be a bug in the host app, whose
|
|
168
|
+
# file and line are the point, and asking for -v means changing a deployed command.
|
|
169
|
+
def self.report_failure(program, error, argv: ARGV)
|
|
170
|
+
warn "#{program}: #{error.message}"
|
|
171
|
+
hint = failure_hint(error)
|
|
172
|
+
warn "#{program}: #{hint}" if hint
|
|
173
|
+
return unless error.backtrace
|
|
174
|
+
return unless failure_trace_wanted?(error, hint) || argv.intersect?(%w[-v --verbose])
|
|
175
|
+
|
|
176
|
+
warn error.backtrace.join("\n")
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def self.failure_trace_wanted?(error, hint) = hint.nil? && !error.is_a?(StartupError)
|
|
180
|
+
private_class_method :failure_trace_wanted?
|
|
181
|
+
|
|
182
|
+
def self.failure_hint(error)
|
|
183
|
+
case error
|
|
184
|
+
when ::RedisClient::ConnectionError
|
|
185
|
+
'could not reach Redis; check REDIS_URL'
|
|
186
|
+
when ->(e) { defined?(::OptionParser::ParseError) && e.is_a?(::OptionParser::ParseError) }
|
|
187
|
+
'see --help'
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
private_class_method :failure_hint
|
|
191
|
+
|
|
136
192
|
# `boot_app:` / `warmup:` are test seams. Production always passes true.
|
|
137
|
-
def run(boot_app: true, warmup: true)
|
|
193
|
+
def run(boot_app: true, warmup: true) # rubocop:disable Metrics/AbcSize
|
|
194
|
+
exit(run_health) if @command == 'health'
|
|
195
|
+
|
|
138
196
|
# Mark server mode BEFORE the app loads so `configure_server` blocks in
|
|
139
197
|
# the required initializer actually fire (they gate on `config.server?`).
|
|
140
198
|
# Matches Sidekiq, which sets `Sidekiq.server = true` before requiring
|
|
@@ -144,11 +202,13 @@ module Wurk
|
|
|
144
202
|
# false`).
|
|
145
203
|
enter_server_mode
|
|
146
204
|
boot_application if boot_app
|
|
205
|
+
Swarm::TestModeGuard.enforce!(@config.logger)
|
|
147
206
|
self_read, self_write = ::IO.pipe
|
|
148
207
|
begin
|
|
149
208
|
trap_signals(self_write)
|
|
150
209
|
validate_redis!
|
|
151
210
|
validate_pool_sizes!
|
|
211
|
+
BootChecks.run(@config, redis_info)
|
|
152
212
|
@config[:identity] = identity
|
|
153
213
|
# Force lazy server-middleware chain so worker threads don't race
|
|
154
214
|
# against each other constructing it. Spec: Sidekiq::CLI line 104.
|
|
@@ -175,23 +235,28 @@ module Wurk
|
|
|
175
235
|
# (`WURK_PRELOAD`/`SIDEKIQ_PRELOAD` Bundler groups, `WURK_PRELOAD_APP`/
|
|
176
236
|
# `SIDEKIQ_PRELOAD_APP` whole-app eager-load) and boots `Process.warmup`
|
|
177
237
|
# before the fork so children share warmed pages (copy-on-write). Spec §7.
|
|
178
|
-
def run_swarm(boot_app: true, warmup: true)
|
|
179
|
-
raise
|
|
238
|
+
def run_swarm(boot_app: true, warmup: true) # rubocop:disable Metrics/AbcSize
|
|
239
|
+
raise StartupError, "the swarm runner takes no subcommand; run `wurk #{@command}` instead" if @command
|
|
180
240
|
|
|
181
241
|
# Server mode before the app loads — see #run. The flag rides through the
|
|
182
242
|
# fork into every child (the config object is copied), so configure_server
|
|
183
243
|
# blocks registered in the parent take effect in the workers.
|
|
184
244
|
enter_server_mode
|
|
245
|
+
# Only the children read it. Rejected here, a typo fails this boot instead of
|
|
246
|
+
# crash-looping the fleet until the crash-loop exit.
|
|
247
|
+
Health.options_from_env
|
|
185
248
|
if boot_app
|
|
249
|
+
load_rails_framework
|
|
186
250
|
preload_bundler_groups
|
|
187
251
|
boot_application
|
|
188
252
|
eager_load_application
|
|
189
253
|
end
|
|
190
254
|
validate_redis!
|
|
191
255
|
validate_pool_sizes!
|
|
256
|
+
BootChecks.run(@config, redis_info)
|
|
192
257
|
@config[:identity] = identity
|
|
193
258
|
warm_up_process if warmup
|
|
194
|
-
@swarm = Wurk::Swarm.new(topology: @config.topology, config: @config,
|
|
259
|
+
@swarm = Wurk::Swarm.new(topology: @config.topology, config: @config, exit_on_crash_loop: true,
|
|
195
260
|
shutdown_timeout: @config[:timeout] || Swarm::DEFAULT_SHUTDOWN_TIMEOUT)
|
|
196
261
|
begin
|
|
197
262
|
@swarm.boot(install_signals: true)
|
|
@@ -202,6 +267,16 @@ module Wurk
|
|
|
202
267
|
# A no-op once the loop already drained (no children, pipe closed).
|
|
203
268
|
@swarm.shutdown
|
|
204
269
|
end
|
|
270
|
+
exit(1) if @swarm.crash_looped?
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
# `wurk health` — see Wurk::HealthProbe. Boots nothing; returns the exit code.
|
|
274
|
+
def run_health
|
|
275
|
+
require_relative 'health_probe'
|
|
276
|
+
opts = @health_options
|
|
277
|
+
@config.redis = { url: opts[:health_redis_url] } if opts[:health_redis_url]
|
|
278
|
+
HealthProbe.new(@config, window: opts[:health_window] || HealthProbe::DEFAULT_WINDOW,
|
|
279
|
+
min: opts[:health_min] || 1, strict: opts[:health_strict] == true).call
|
|
205
280
|
end
|
|
206
281
|
|
|
207
282
|
# `wurk api` — serve the machine-facing HTTP API and nothing else: no
|
|
@@ -223,7 +298,7 @@ module Wurk
|
|
|
223
298
|
# Load the app here, not on the first request: a broken load should fail
|
|
224
299
|
# the boot an operator is watching, not the first client to call.
|
|
225
300
|
require_relative 'api/app'
|
|
226
|
-
raise
|
|
301
|
+
raise StartupError, NO_API_TOKEN_MESSAGE unless @config.api_enabled?
|
|
227
302
|
|
|
228
303
|
handler ||= api_handler(@config[:api_server])
|
|
229
304
|
logger.info { "Wurk API listening on http://#{api_bind}:#{api_port}#{Wurk::API::VERSION_PREFIX}" }
|
|
@@ -270,8 +345,8 @@ module Wurk
|
|
|
270
345
|
registry = handler_registry
|
|
271
346
|
name ? registry.get(name) : registry.default
|
|
272
347
|
rescue ::LoadError, ::NameError, ::ArgumentError => e
|
|
273
|
-
raise
|
|
274
|
-
|
|
348
|
+
raise StartupError, "wurk api could not start a web server (#{e.message}). Add one to your Gemfile " \
|
|
349
|
+
'— puma, falcon, or rackup + webrick — or name an installed one with --server.'
|
|
275
350
|
end
|
|
276
351
|
|
|
277
352
|
def handler_registry
|
|
@@ -323,14 +398,15 @@ module Wurk
|
|
|
323
398
|
end
|
|
324
399
|
|
|
325
400
|
def signal_names
|
|
326
|
-
|
|
401
|
+
# INFO exists on BSD and macOS only; asking for it on Linux just warned at every boot.
|
|
402
|
+
%w[INT TERM TSTP TTIN INFO USR2].select { |name| ::Signal.list.key?(name) }
|
|
327
403
|
end
|
|
328
404
|
|
|
329
405
|
def reopen_logs
|
|
330
406
|
log = logger
|
|
331
407
|
log.reopen if log.respond_to?(:reopen)
|
|
332
|
-
rescue StandardError
|
|
333
|
-
|
|
408
|
+
rescue StandardError => e
|
|
409
|
+
warn "wurk: USR2 log reopen failed, still writing to the previous log: #{e.class}: #{e.message}"
|
|
334
410
|
end
|
|
335
411
|
|
|
336
412
|
def validate_redis!
|
|
@@ -358,7 +434,7 @@ module Wurk
|
|
|
358
434
|
|
|
359
435
|
def validate_pool_sizes!
|
|
360
436
|
@config.capsules.each_pair do |name, cap|
|
|
361
|
-
raise
|
|
437
|
+
raise StartupError, "Pool size too small for #{name}" if cap.redis_pool.size < cap.concurrency
|
|
362
438
|
end
|
|
363
439
|
end
|
|
364
440
|
|
|
@@ -372,6 +448,14 @@ module Wurk
|
|
|
372
448
|
COMMANDS.include?(args.first) ? args.shift : nil
|
|
373
449
|
end
|
|
374
450
|
|
|
451
|
+
def parse_health_options(args)
|
|
452
|
+
@health_options = {}
|
|
453
|
+
parser = option_parser(@health_options)
|
|
454
|
+
@parser = parser
|
|
455
|
+
parser.parse!(args)
|
|
456
|
+
self
|
|
457
|
+
end
|
|
458
|
+
|
|
375
459
|
def setup_options(args)
|
|
376
460
|
opts = parse_options(args)
|
|
377
461
|
set_environment(opts[:environment])
|
|
@@ -416,7 +500,7 @@ module Wurk
|
|
|
416
500
|
end
|
|
417
501
|
|
|
418
502
|
def parse_config(path)
|
|
419
|
-
raise
|
|
503
|
+
raise StartupError, "No such file #{path}" unless ::File.exist?(path)
|
|
420
504
|
|
|
421
505
|
require 'erb'
|
|
422
506
|
require 'yaml'
|
|
@@ -451,6 +535,7 @@ module Wurk
|
|
|
451
535
|
parser = option_parser(opts)
|
|
452
536
|
@parser = parser
|
|
453
537
|
parser.parse!(argv)
|
|
538
|
+
warn "wurk: ignoring #{argv.join(' ')}: not a command or a flag (see --help)" unless argv.empty?
|
|
454
539
|
opts
|
|
455
540
|
end
|
|
456
541
|
|
|
@@ -458,16 +543,37 @@ module Wurk
|
|
|
458
543
|
require 'optparse'
|
|
459
544
|
|
|
460
545
|
::OptionParser.new do |o|
|
|
461
|
-
o.banner =
|
|
462
|
-
|
|
546
|
+
o.banner = help_banner
|
|
547
|
+
o.separator help_intro if help_intro
|
|
548
|
+
define_value_flags(o, opts, @command == 'health' ? HEALTH_OPTION_FLAGS : OPTION_FLAGS)
|
|
463
549
|
define_value_flags(o, opts, API_OPTION_FLAGS) if @command == 'api'
|
|
464
550
|
define_meta_flags(o, opts)
|
|
465
551
|
end
|
|
466
552
|
end
|
|
467
553
|
|
|
554
|
+
def swarm_binary? = ::File.basename($PROGRAM_NAME).include?('swarm')
|
|
555
|
+
|
|
556
|
+
def help_banner
|
|
557
|
+
return "wurk #{@command} [options]" if @command
|
|
558
|
+
return "#{::File.basename($PROGRAM_NAME)} [options]" if swarm_binary?
|
|
559
|
+
|
|
560
|
+
"wurk [options]\nwurk COMMAND [options]"
|
|
561
|
+
end
|
|
562
|
+
|
|
563
|
+
def help_intro
|
|
564
|
+
return HELP_API_NOTE if @command == 'api'
|
|
565
|
+
return if @command
|
|
566
|
+
|
|
567
|
+
swarm_binary? ? HELP_SWARM_NOTE : HELP_COMMANDS
|
|
568
|
+
end
|
|
569
|
+
|
|
468
570
|
def define_value_flags(parser, opts, flags)
|
|
469
571
|
flags.each do |short, long, key, desc, transform|
|
|
470
|
-
parser.on(short, long, desc)
|
|
572
|
+
parser.on(short, long, desc) do |arg|
|
|
573
|
+
assign_flag(opts, key, arg, transform)
|
|
574
|
+
rescue ArgumentError
|
|
575
|
+
raise StartupError, "#{long.split.first}: #{arg.inspect} is not a number"
|
|
576
|
+
end
|
|
471
577
|
end
|
|
472
578
|
end
|
|
473
579
|
|
|
@@ -485,19 +591,78 @@ module Wurk
|
|
|
485
591
|
exit(0)
|
|
486
592
|
end
|
|
487
593
|
parser.on_tail('-h', '--help', 'Show help') do
|
|
488
|
-
puts parser
|
|
594
|
+
puts parser, HELP_FOOTER
|
|
489
595
|
exit(0)
|
|
490
596
|
end
|
|
491
597
|
end
|
|
492
598
|
|
|
493
599
|
# --- application bootstrap --------------------------------------------
|
|
494
600
|
|
|
601
|
+
# A Rails target has two shapes that need opposite orders. A gem may need Rails
|
|
602
|
+
# loaded while it requires: `config/application.rb` requires the framework and
|
|
603
|
+
# only then calls `Bundler.require`, so running the app's first half before the
|
|
604
|
+
# group preload suits it. An app may instead use a gem it expects the swarm to have
|
|
605
|
+
# preloaded, as stock sidekiqswarm's order (spec §7.3) gives it, and then that
|
|
606
|
+
# file raises NameError here. Only the file can tell us which it is, and by then
|
|
607
|
+
# its class body may have half-run, registering initializers and middleware that a
|
|
608
|
+
# second load in this process would register again. So the retry is a fresh process:
|
|
609
|
+
# same pid, same environment as before the attempt, preload first. Once only, so a
|
|
610
|
+
# genuine bug surfaces with its own message.
|
|
611
|
+
def load_rails_framework
|
|
612
|
+
root = rails_app_root(@config[:require])
|
|
613
|
+
return unless root
|
|
614
|
+
|
|
615
|
+
restart = restart_state
|
|
616
|
+
first_error = ENV.delete(PRELOAD_FIRST_ENV)
|
|
617
|
+
ENV['RACK_ENV'] = ENV['RAILS_ENV'] = @environment
|
|
618
|
+
require 'rails'
|
|
619
|
+
define_active_job_adapter
|
|
620
|
+
return note_preload_first(first_error) if first_error
|
|
621
|
+
|
|
622
|
+
begin
|
|
623
|
+
require ::File.join(root, 'config/application.rb')
|
|
624
|
+
rescue ::NameError => e
|
|
625
|
+
restart_with_preload_first(restart, e)
|
|
626
|
+
end
|
|
627
|
+
end
|
|
628
|
+
|
|
629
|
+
def restart_state
|
|
630
|
+
{ env: ::ENV.to_h, argv: ::ARGV.dup, dir: ::Dir.pwd }
|
|
631
|
+
end
|
|
632
|
+
|
|
633
|
+
def note_preload_first(first_error)
|
|
634
|
+
logger.warn do
|
|
635
|
+
"config/application.rb raised before the preload (#{first_error}), so this boot preloads first. " \
|
|
636
|
+
'If it fails too, that first error may be the one to fix.'
|
|
637
|
+
end
|
|
638
|
+
end
|
|
639
|
+
|
|
640
|
+
# Bundler may have arrived only inside the failed attempt (`config/boot.rb`), and the
|
|
641
|
+
# snapshot predates it, so the fresh process is told about it explicitly. If the
|
|
642
|
+
# exec itself fails, the app's error is still the one worth reporting.
|
|
643
|
+
def restart_with_preload_first(state, error)
|
|
644
|
+
env = state[:env].merge(PRELOAD_FIRST_ENV => error.message.lines.first.to_s.strip[0, 300])
|
|
645
|
+
env['BUNDLE_GEMFILE'] ||= ::Bundler.default_gemfile.to_s if defined?(::Bundler)
|
|
646
|
+
ruby = [::RbConfig.ruby, *('-rbundler/setup' if defined?(::Bundler))]
|
|
647
|
+
::Process.exec(env, *ruby, SWARM_EXECUTABLE, *state[:argv], unsetenv_others: true, chdir: state[:dir])
|
|
648
|
+
rescue ::SystemCallError
|
|
649
|
+
raise error
|
|
650
|
+
end
|
|
651
|
+
|
|
652
|
+
# The app root for a `-r` that is the app directory or its environment file.
|
|
653
|
+
def rails_app_root(require_path)
|
|
654
|
+
path = ::File.expand_path(require_path.to_s)
|
|
655
|
+
path = ::File.dirname(path, 2) if ::File.basename(path) == 'environment.rb'
|
|
656
|
+
path if ::File.directory?(path) && ::File.exist?(::File.join(path, 'config/application.rb'))
|
|
657
|
+
end
|
|
658
|
+
|
|
495
659
|
# Spec §7.2/§7.3. Before the swarm parent forks, `Bundler.require` the
|
|
496
660
|
# configured groups so their gems are resident in the parent and paged
|
|
497
661
|
# copy-on-write into every child. Comma-separated group list; the native
|
|
498
662
|
# `WURK_PRELOAD` wins over the `SIDEKIQ_PRELOAD` drop-in alias. Defaults to
|
|
499
663
|
# the `default` group; an explicit empty value (`WURK_PRELOAD=`) disables
|
|
500
|
-
# the preload. Runs before `boot_application
|
|
664
|
+
# the preload. Runs before `boot_application`, so before the initializers; see
|
|
665
|
+
# #load_rails_framework for where `config/application.rb` falls.
|
|
501
666
|
def preload_bundler_groups
|
|
502
667
|
return unless defined?(::Bundler)
|
|
503
668
|
|
|
@@ -554,6 +719,9 @@ module Wurk
|
|
|
554
719
|
def boot_rails_application(require_path)
|
|
555
720
|
require 'rails'
|
|
556
721
|
define_active_job_adapter
|
|
722
|
+
# No railtie here (see boot_application), and a host's after_initialize hook
|
|
723
|
+
# may wrap the reloader, so it has to be the Rails one by then.
|
|
724
|
+
::ActiveSupport.on_load(:after_initialize, yield: true) { |app| RailsBoot.install_reloader(app) }
|
|
557
725
|
require ::File.expand_path("#{require_path}/config/environment.rb")
|
|
558
726
|
@config[:tag] ||= default_tag(::Rails.root) if defined?(::Rails) && ::Rails.respond_to?(:root)
|
|
559
727
|
end
|
|
@@ -576,23 +744,29 @@ module Wurk
|
|
|
576
744
|
end
|
|
577
745
|
|
|
578
746
|
def validate!
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
bad_rails = ::File.directory?(require_path.to_s) &&
|
|
582
|
-
!::File.exist?(::File.join(require_path, 'config/application.rb'))
|
|
583
|
-
return print_help_and_die if bad_path || bad_rails
|
|
747
|
+
reason = require_path_problem(@config[:require].to_s)
|
|
748
|
+
return print_help_and_die(reason) if reason
|
|
584
749
|
|
|
585
750
|
%i[concurrency timeout].each do |opt|
|
|
586
|
-
raise
|
|
751
|
+
raise StartupError, "#{opt}: #{@config[opt]} is not a valid value" if @config[opt].to_i <= 0
|
|
587
752
|
end
|
|
588
753
|
end
|
|
589
754
|
|
|
590
|
-
def
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
755
|
+
def require_path_problem(path)
|
|
756
|
+
return 'no such file or directory' unless ::File.exist?(path)
|
|
757
|
+
|
|
758
|
+
return unless ::File.directory?(path) && !::File.exist?(::File.join(path, 'config/application.rb'))
|
|
759
|
+
|
|
760
|
+
'it has no config/application.rb'
|
|
761
|
+
end
|
|
762
|
+
|
|
763
|
+
def print_help_and_die(reason)
|
|
764
|
+
logger.error '=================================================================='
|
|
765
|
+
logger.error " Wurk cannot load job classes from #{@config[:require]}: #{reason}."
|
|
766
|
+
logger.error ' Please point Wurk to a Rails application or a Ruby file'
|
|
767
|
+
logger.error ' to load your job classes with -r [DIR|FILE].'
|
|
768
|
+
logger.error '=================================================================='
|
|
769
|
+
logger.error @parser.to_s
|
|
596
770
|
exit(1)
|
|
597
771
|
end
|
|
598
772
|
end
|