wurk 1.9.0 → 1.10.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/app/controllers/wurk/api_controller.rb +4 -1
- data/lib/wurk/boot_checks.rb +87 -0
- data/lib/wurk/capsule.rb +33 -8
- data/lib/wurk/cli.rb +73 -8
- data/lib/wurk/client/buffered.rb +240 -26
- data/lib/wurk/compat.rb +11 -0
- data/lib/wurk/configuration.rb +114 -16
- data/lib/wurk/cron.rb +45 -0
- data/lib/wurk/debounce.rb +2 -2
- 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 +9 -3
- 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/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 +42 -31
- data/lib/wurk/swarm/crash_watch.rb +65 -0
- data/lib/wurk/swarm.rb +79 -11
- data/lib/wurk/testing.rb +66 -0
- data/lib/wurk/version.rb +1 -1
- data/lib/wurk/web/extension.rb +6 -1
- 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 +41 -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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ce85d34d0d8e1f5aa0837c76310a94c2ca3e584ff0e9ca4d9e977c0347510f89
|
|
4
|
+
data.tar.gz: 113f78f30121c728a4d7f3db5dc6d1472f13ab1b6b7b50d6ee73223d681585f4
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ca00027cf185252fd95e78c3283a90d4a0c21acbc3cd6623bc1a4a1e2add8b6625d8436f60fc80b934a2fb35325d013903791dcad3606605b580b7633958fa9f
|
|
7
|
+
data.tar.gz: bcae6990d734afd7dbc30e951aead9b7d35b77d060c1198a11d4d34d17ba866e6e81ba1ab168a417b115c1a6a690e8cc4c9926951fec58615d7c2e40a9619fda
|
|
@@ -62,7 +62,10 @@ module Wurk
|
|
|
62
62
|
end
|
|
63
63
|
|
|
64
64
|
def queues
|
|
65
|
-
|
|
65
|
+
consumers = ::Wurk.redis(idempotent: true) { |conn| ::Wurk::QueueConsumers.counts(conn) }
|
|
66
|
+
render json: ::Wurk::Stats.new.queue_summaries.map { |q|
|
|
67
|
+
::Wurk::Api::Serializers.queue_summary(q).merge(consumers: consumers.fetch(q.name, 0))
|
|
68
|
+
}
|
|
66
69
|
end
|
|
67
70
|
|
|
68
71
|
def queue
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'keys'
|
|
4
|
+
require_relative 'queue_consumers'
|
|
5
|
+
|
|
6
|
+
module Wurk
|
|
7
|
+
# What a standalone runner says once at boot, after the app loaded. Log only,
|
|
8
|
+
# and never allowed to stop a boot: none of this changes behaviour by backend,
|
|
9
|
+
# version or peers.
|
|
10
|
+
module BootChecks
|
|
11
|
+
# Oldest version each non-Redis backend has been run against.
|
|
12
|
+
TESTED_FLOORS = { 'dragonfly' => '1.34.0', 'valkey' => '8.0.0' }.freeze
|
|
13
|
+
MAX_LISTED = 20
|
|
14
|
+
|
|
15
|
+
module_function
|
|
16
|
+
|
|
17
|
+
def run(config, info)
|
|
18
|
+
log_backend(config.logger, info)
|
|
19
|
+
warn_on_foreign_processes(config)
|
|
20
|
+
warn_on_ignored_locks(config.logger)
|
|
21
|
+
rescue StandardError => e
|
|
22
|
+
config.logger.warn { "wurk boot checks skipped: #{e.class}: #{e.message}" }
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def backend(info)
|
|
26
|
+
return ['dragonfly', info['dragonfly_version'].to_s.delete_prefix('df-v')] if info['dragonfly_version']
|
|
27
|
+
return ['valkey', info['valkey_version'].to_s] if info['valkey_version']
|
|
28
|
+
|
|
29
|
+
['redis', info['redis_version'].to_s]
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def log_backend(logger, info)
|
|
33
|
+
name, version = backend(info)
|
|
34
|
+
logger.info { "Wurk #{Wurk::VERSION} on #{name} #{version}" }
|
|
35
|
+
floor = TESTED_FLOORS[name]
|
|
36
|
+
return unless floor && ::Gem::Version.correct?(version) && ::Gem::Version.new(version) < ::Gem::Version.new(floor)
|
|
37
|
+
|
|
38
|
+
logger.warn { "#{name} #{version} is older than the oldest version Wurk is tested against (#{floor})." }
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Both runtimes share `processes` and the same queues, so a mix is safe only
|
|
42
|
+
# if it is deliberate. Wurk's heartbeat always carries `cores`; stock does not.
|
|
43
|
+
def warn_on_foreign_processes(config)
|
|
44
|
+
foreign = process_infos(config).reject { |_, info| info.nil? || ::JSON.parse(info).key?('cores') }.map(&:first)
|
|
45
|
+
return if foreign.empty?
|
|
46
|
+
|
|
47
|
+
config.logger.warn do
|
|
48
|
+
"Live processes that are not Wurk share this Redis: #{listing(foreign)}. " \
|
|
49
|
+
'They fetch from the same queues. If this is not a deliberate rolling cutover, stop them.'
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def process_infos(config)
|
|
54
|
+
config.redis(idempotent: true) do |conn|
|
|
55
|
+
ids = conn.call('SMEMBERS', Keys::PROCESSES)
|
|
56
|
+
ids.empty? ? [] : ids.zip(conn.pipelined { |pipe| ids.each { |id| pipe.call('HGET', id, 'info') } })
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Without the gem's middleware the option does nothing: the "unique" job
|
|
61
|
+
# simply enqueues twice. Only classes the app has loaded by now are seen.
|
|
62
|
+
def warn_on_ignored_locks(logger)
|
|
63
|
+
return if defined?(::SidekiqUniqueJobs)
|
|
64
|
+
|
|
65
|
+
classes = locked_job_classes
|
|
66
|
+
return if classes.empty?
|
|
67
|
+
|
|
68
|
+
logger.warn do
|
|
69
|
+
'These jobs set `lock:` but sidekiq-unique-jobs is not loaded, so the option does nothing and ' \
|
|
70
|
+
"duplicates will run: #{listing(classes)}. Keep the gem, or move them to `unique_for:`."
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def locked_job_classes
|
|
75
|
+
::ObjectSpace.each_object(::Class).filter_map do |klass|
|
|
76
|
+
next unless klass.name && klass < Wurk::Worker
|
|
77
|
+
|
|
78
|
+
klass.name if klass.get_sidekiq_options.key?('lock')
|
|
79
|
+
end.sort
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def listing(names)
|
|
83
|
+
extra = names.size - MAX_LISTED
|
|
84
|
+
names.first(MAX_LISTED).join(', ') + (extra.positive? ? " and #{extra} more" : '')
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
data/lib/wurk/capsule.rb
CHANGED
|
@@ -53,6 +53,8 @@ module Wurk
|
|
|
53
53
|
# %w[high default low] → mode :strict, weights all 0
|
|
54
54
|
# %w[high,3 default,2 low,1] → mode :weighted, weights {q=>w}
|
|
55
55
|
# %w[a,1 b,1 c,1] → mode :random, weights all 1
|
|
56
|
+
# %w[a,2 b] → mode :weighted, unweighted b fetched as if 1
|
|
57
|
+
# %w[a,0 b,0] → mode :strict, 0 being the same as no weight
|
|
56
58
|
# `@queues` is expanded by weight so a uniform shuffle gives weighted
|
|
57
59
|
# fairness (e.g. ["high","high","high","default","default","low"]).
|
|
58
60
|
def queues=(val)
|
|
@@ -188,6 +190,16 @@ module Wurk
|
|
|
188
190
|
@pools.clear
|
|
189
191
|
end
|
|
190
192
|
|
|
193
|
+
# The fork-path form: closes idle sockets but keeps every pool usable, for a
|
|
194
|
+
# parent whose own threads may be mid-checkout.
|
|
195
|
+
def reload_redis_pools!
|
|
196
|
+
@pools.each_value(&:reload!)
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def redis_pools_built?
|
|
200
|
+
!@pools.empty?
|
|
201
|
+
end
|
|
202
|
+
|
|
191
203
|
def redis(idempotent: false, &)
|
|
192
204
|
PoolCheckout.with(redis_pool, idempotent, &)
|
|
193
205
|
end
|
|
@@ -229,19 +241,32 @@ module Wurk
|
|
|
229
241
|
copy = server_middleware
|
|
230
242
|
return copy if frozen?
|
|
231
243
|
|
|
232
|
-
@config.server_middleware.entries
|
|
244
|
+
global = @config.server_middleware.entries
|
|
245
|
+
global.each_with_index do |entry, index|
|
|
233
246
|
next if @server_middleware_seen.include?(entry.klass)
|
|
234
247
|
|
|
235
248
|
@server_middleware_seen << entry.klass
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
rebound = entry.dup
|
|
239
|
-
rebound.config = self
|
|
240
|
-
copy.entries << rebound
|
|
249
|
+
adopt_server_middleware(copy, entry, global.drop(index + 1)) unless copy.exists?(entry.klass)
|
|
241
250
|
end
|
|
242
251
|
copy
|
|
243
252
|
end
|
|
244
253
|
|
|
254
|
+
def adopt_server_middleware(copy, entry, followers)
|
|
255
|
+
rebound = entry.dup
|
|
256
|
+
rebound.config = self
|
|
257
|
+
copy.entries.insert(refresh_position(copy, followers), rebound)
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
# Ahead of the first entry that follows it in the global chain, so a late
|
|
261
|
+
# `prepend` still wraps what it was prepended to. -1 appends.
|
|
262
|
+
def refresh_position(copy, followers)
|
|
263
|
+
followers.each do |follower|
|
|
264
|
+
position = copy.entries.index { |entry| entry.klass == follower.klass }
|
|
265
|
+
return position if position
|
|
266
|
+
end
|
|
267
|
+
-1
|
|
268
|
+
end
|
|
269
|
+
|
|
245
270
|
# Drop-in fetch pluggability (spec §5 / §4.1): instantiate
|
|
246
271
|
# `config[:fetch_class]` when the host set one — a custom or Pro fetcher —
|
|
247
272
|
# else the default reliable BLMOVE fetcher (`Sidekiq::BasicFetch`). A
|
|
@@ -276,7 +301,7 @@ module Wurk
|
|
|
276
301
|
return [qname, 0] if weight.nil?
|
|
277
302
|
|
|
278
303
|
weight = Integer(weight)
|
|
279
|
-
raise ArgumentError, "queue weight must be
|
|
304
|
+
raise ArgumentError, "queue weight must not be negative: `#{entry}`" if weight.negative?
|
|
280
305
|
|
|
281
306
|
[qname, weight]
|
|
282
307
|
end
|
|
@@ -295,7 +320,7 @@ module Wurk
|
|
|
295
320
|
def expand_by_weight(parsed, mode)
|
|
296
321
|
return parsed.map(&:first) if %i[strict random].include?(mode)
|
|
297
322
|
|
|
298
|
-
parsed.flat_map { |q, w| [q] * w }
|
|
323
|
+
parsed.flat_map { |q, w| [q] * [w, 1].max }
|
|
299
324
|
end
|
|
300
325
|
|
|
301
326
|
# `config.redis = { size: N }` pins the main pool at N; otherwise
|
data/lib/wurk/cli.rb
CHANGED
|
@@ -11,6 +11,8 @@ require_relative 'configuration'
|
|
|
11
11
|
require_relative 'component'
|
|
12
12
|
require_relative 'capsule'
|
|
13
13
|
require_relative 'launcher'
|
|
14
|
+
require_relative 'rails_boot'
|
|
15
|
+
require_relative 'boot_checks'
|
|
14
16
|
|
|
15
17
|
module Wurk
|
|
16
18
|
# Standalone CLI. Loads from `exe/wurk` — never loads `wurk/rails` so the
|
|
@@ -29,7 +31,7 @@ module Wurk
|
|
|
29
31
|
# Subcommands. Bare `wurk` runs the worker — the historical shape, and the
|
|
30
32
|
# only one the swarm binaries accept — so a subcommand is never anything
|
|
31
33
|
# but the first argument, and every argument after it is that command's.
|
|
32
|
-
COMMANDS = %w[api].freeze
|
|
34
|
+
COMMANDS = %w[api health].freeze
|
|
33
35
|
|
|
34
36
|
# `wurk api` defaults. 0.0.0.0 because the point of the machine API is for
|
|
35
37
|
# another service to reach it; it is bearer-gated, and answers 404 to
|
|
@@ -99,6 +101,15 @@ module Wurk
|
|
|
99
101
|
['-s', '--server NAME', :api_server, 'Rack handler to serve with (default: the first installed)']
|
|
100
102
|
].freeze
|
|
101
103
|
|
|
104
|
+
HEALTH_OPTION_FLAGS = [
|
|
105
|
+
['-w', '--window SECONDS', :health_window, 'A heartbeat older than this does not count (default 60)', :to_i],
|
|
106
|
+
['-m', '--min COUNT', :health_min, 'Processes on this host that must be beating (default 1)', :to_i],
|
|
107
|
+
['-u', '--redis-url URL', :health_redis_url, 'Redis to read (default REDIS_URL)'],
|
|
108
|
+
['-s', '--strict', :health_strict, 'Fail when Redis cannot be read, rather than passing']
|
|
109
|
+
].freeze
|
|
110
|
+
|
|
111
|
+
HELP_COMMANDS = "\nCommands:\n health Exit 0 if this host's workers are heartbeating\n\nOptions:"
|
|
112
|
+
|
|
102
113
|
attr_accessor :launcher, :environment, :config
|
|
103
114
|
|
|
104
115
|
# The subcommand `parse` pulled off argv, or nil for the worker runner.
|
|
@@ -127,6 +138,8 @@ module Wurk
|
|
|
127
138
|
def parse(args = ARGV.dup)
|
|
128
139
|
@config ||= Wurk.default_configuration
|
|
129
140
|
@command = extract_command!(args)
|
|
141
|
+
return parse_health_options(args) if @command == 'health'
|
|
142
|
+
|
|
130
143
|
setup_options(args)
|
|
131
144
|
initialize_logger
|
|
132
145
|
validate!
|
|
@@ -134,7 +147,9 @@ module Wurk
|
|
|
134
147
|
end
|
|
135
148
|
|
|
136
149
|
# `boot_app:` / `warmup:` are test seams. Production always passes true.
|
|
137
|
-
def run(boot_app: true, warmup: true)
|
|
150
|
+
def run(boot_app: true, warmup: true) # rubocop:disable Metrics/AbcSize
|
|
151
|
+
exit(run_health) if @command == 'health'
|
|
152
|
+
|
|
138
153
|
# Mark server mode BEFORE the app loads so `configure_server` blocks in
|
|
139
154
|
# the required initializer actually fire (they gate on `config.server?`).
|
|
140
155
|
# Matches Sidekiq, which sets `Sidekiq.server = true` before requiring
|
|
@@ -149,6 +164,7 @@ module Wurk
|
|
|
149
164
|
trap_signals(self_write)
|
|
150
165
|
validate_redis!
|
|
151
166
|
validate_pool_sizes!
|
|
167
|
+
BootChecks.run(@config, redis_info)
|
|
152
168
|
@config[:identity] = identity
|
|
153
169
|
# Force lazy server-middleware chain so worker threads don't race
|
|
154
170
|
# against each other constructing it. Spec: Sidekiq::CLI line 104.
|
|
@@ -175,23 +191,28 @@ module Wurk
|
|
|
175
191
|
# (`WURK_PRELOAD`/`SIDEKIQ_PRELOAD` Bundler groups, `WURK_PRELOAD_APP`/
|
|
176
192
|
# `SIDEKIQ_PRELOAD_APP` whole-app eager-load) and boots `Process.warmup`
|
|
177
193
|
# before the fork so children share warmed pages (copy-on-write). Spec §7.
|
|
178
|
-
def run_swarm(boot_app: true, warmup: true)
|
|
194
|
+
def run_swarm(boot_app: true, warmup: true) # rubocop:disable Metrics/AbcSize
|
|
179
195
|
raise ArgumentError, "the swarm runner takes no subcommand; run `wurk #{@command}` instead" if @command
|
|
180
196
|
|
|
181
197
|
# Server mode before the app loads — see #run. The flag rides through the
|
|
182
198
|
# fork into every child (the config object is copied), so configure_server
|
|
183
199
|
# blocks registered in the parent take effect in the workers.
|
|
184
200
|
enter_server_mode
|
|
201
|
+
# Only the children read it. Rejected here, a typo fails this boot instead of
|
|
202
|
+
# crash-looping the fleet until the crash-loop exit.
|
|
203
|
+
Health.options_from_env
|
|
185
204
|
if boot_app
|
|
205
|
+
load_rails_framework
|
|
186
206
|
preload_bundler_groups
|
|
187
207
|
boot_application
|
|
188
208
|
eager_load_application
|
|
189
209
|
end
|
|
190
210
|
validate_redis!
|
|
191
211
|
validate_pool_sizes!
|
|
212
|
+
BootChecks.run(@config, redis_info)
|
|
192
213
|
@config[:identity] = identity
|
|
193
214
|
warm_up_process if warmup
|
|
194
|
-
@swarm = Wurk::Swarm.new(topology: @config.topology, config: @config,
|
|
215
|
+
@swarm = Wurk::Swarm.new(topology: @config.topology, config: @config, exit_on_crash_loop: true,
|
|
195
216
|
shutdown_timeout: @config[:timeout] || Swarm::DEFAULT_SHUTDOWN_TIMEOUT)
|
|
196
217
|
begin
|
|
197
218
|
@swarm.boot(install_signals: true)
|
|
@@ -202,6 +223,16 @@ module Wurk
|
|
|
202
223
|
# A no-op once the loop already drained (no children, pipe closed).
|
|
203
224
|
@swarm.shutdown
|
|
204
225
|
end
|
|
226
|
+
exit(1) if @swarm.crash_looped?
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
# `wurk health` — see Wurk::HealthProbe. Boots nothing; returns the exit code.
|
|
230
|
+
def run_health
|
|
231
|
+
require_relative 'health_probe'
|
|
232
|
+
opts = @health_options
|
|
233
|
+
@config.redis = { url: opts[:health_redis_url] } if opts[:health_redis_url]
|
|
234
|
+
HealthProbe.new(@config, window: opts[:health_window] || HealthProbe::DEFAULT_WINDOW,
|
|
235
|
+
min: opts[:health_min] || 1, strict: opts[:health_strict] == true).call
|
|
205
236
|
end
|
|
206
237
|
|
|
207
238
|
# `wurk api` — serve the machine-facing HTTP API and nothing else: no
|
|
@@ -329,8 +360,8 @@ module Wurk
|
|
|
329
360
|
def reopen_logs
|
|
330
361
|
log = logger
|
|
331
362
|
log.reopen if log.respond_to?(:reopen)
|
|
332
|
-
rescue StandardError
|
|
333
|
-
|
|
363
|
+
rescue StandardError => e
|
|
364
|
+
warn "wurk: USR2 log reopen failed, still writing to the previous log: #{e.class}: #{e.message}"
|
|
334
365
|
end
|
|
335
366
|
|
|
336
367
|
def validate_redis!
|
|
@@ -372,6 +403,14 @@ module Wurk
|
|
|
372
403
|
COMMANDS.include?(args.first) ? args.shift : nil
|
|
373
404
|
end
|
|
374
405
|
|
|
406
|
+
def parse_health_options(args)
|
|
407
|
+
@health_options = {}
|
|
408
|
+
parser = option_parser(@health_options)
|
|
409
|
+
@parser = parser
|
|
410
|
+
parser.parse!(args)
|
|
411
|
+
self
|
|
412
|
+
end
|
|
413
|
+
|
|
375
414
|
def setup_options(args)
|
|
376
415
|
opts = parse_options(args)
|
|
377
416
|
set_environment(opts[:environment])
|
|
@@ -458,8 +497,9 @@ module Wurk
|
|
|
458
497
|
require 'optparse'
|
|
459
498
|
|
|
460
499
|
::OptionParser.new do |o|
|
|
461
|
-
o.banner = @command ? "wurk #{@command} [options]" :
|
|
462
|
-
|
|
500
|
+
o.banner = @command ? "wurk #{@command} [options]" : "wurk [options]\nwurk COMMAND [options]"
|
|
501
|
+
o.separator HELP_COMMANDS unless @command
|
|
502
|
+
define_value_flags(o, opts, @command == 'health' ? HEALTH_OPTION_FLAGS : OPTION_FLAGS)
|
|
463
503
|
define_value_flags(o, opts, API_OPTION_FLAGS) if @command == 'api'
|
|
464
504
|
define_meta_flags(o, opts)
|
|
465
505
|
end
|
|
@@ -492,6 +532,28 @@ module Wurk
|
|
|
492
532
|
|
|
493
533
|
# --- application bootstrap --------------------------------------------
|
|
494
534
|
|
|
535
|
+
# A Rails app expects Rails to be loaded before its gems: `config/application.rb`
|
|
536
|
+
# requires the framework and only then calls `Bundler.require`, and gems read
|
|
537
|
+
# `defined?(Rails)` or reference ActiveModel while loading. Requiring the groups
|
|
538
|
+
# first broke those gems in this boot path only. So for a Rails target, run
|
|
539
|
+
# the app's own first half before the group preload.
|
|
540
|
+
def load_rails_framework
|
|
541
|
+
root = rails_app_root(@config[:require])
|
|
542
|
+
return unless root
|
|
543
|
+
|
|
544
|
+
ENV['RACK_ENV'] = ENV['RAILS_ENV'] = @environment
|
|
545
|
+
require 'rails'
|
|
546
|
+
define_active_job_adapter
|
|
547
|
+
require ::File.join(root, 'config/application.rb')
|
|
548
|
+
end
|
|
549
|
+
|
|
550
|
+
# The app root for a `-r` that is the app directory or its environment file.
|
|
551
|
+
def rails_app_root(require_path)
|
|
552
|
+
path = ::File.expand_path(require_path.to_s)
|
|
553
|
+
path = ::File.dirname(path, 2) if ::File.basename(path) == 'environment.rb'
|
|
554
|
+
path if ::File.directory?(path) && ::File.exist?(::File.join(path, 'config/application.rb'))
|
|
555
|
+
end
|
|
556
|
+
|
|
495
557
|
# Spec §7.2/§7.3. Before the swarm parent forks, `Bundler.require` the
|
|
496
558
|
# configured groups so their gems are resident in the parent and paged
|
|
497
559
|
# copy-on-write into every child. Comma-separated group list; the native
|
|
@@ -554,6 +616,9 @@ module Wurk
|
|
|
554
616
|
def boot_rails_application(require_path)
|
|
555
617
|
require 'rails'
|
|
556
618
|
define_active_job_adapter
|
|
619
|
+
# No railtie here (see boot_application), and a host's after_initialize hook
|
|
620
|
+
# may wrap the reloader, so it has to be the Rails one by then.
|
|
621
|
+
::ActiveSupport.on_load(:after_initialize, yield: true) { |app| RailsBoot.install_reloader(app) }
|
|
557
622
|
require ::File.expand_path("#{require_path}/config/environment.rb")
|
|
558
623
|
@config[:tag] ||= default_tag(::Rails.root) if defined?(::Rails) && ::Rails.respond_to?(:root)
|
|
559
624
|
end
|