wurk 1.8.0 → 1.9.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 +29 -9
- data/app/controllers/wurk/api_controller.rb +49 -10
- data/config/routes.rb +20 -12
- data/lib/active_job/queue_adapters/wurk_adapter.rb +8 -2
- data/lib/sidekiq/rails.rb +5 -0
- data/lib/wurk/api/read_only.rb +1 -1
- data/lib/wurk/batch/server_middleware.rb +27 -0
- data/lib/wurk/capsule.rb +57 -2
- data/lib/wurk/cli.rb +4 -2
- data/lib/wurk/client.rb +16 -11
- data/lib/wurk/compat.rb +12 -2
- data/lib/wurk/configuration.rb +86 -7
- data/lib/wurk/encryption.rb +47 -8
- data/lib/wurk/fetcher/private_list_key.rb +19 -2
- data/lib/wurk/fetcher/reaper.rb +36 -12
- data/lib/wurk/fetcher/unparseable_keys.rb +22 -7
- data/lib/wurk/fetcher.rb +6 -1
- data/lib/wurk/health.rb +5 -1
- data/lib/wurk/job_logger.rb +1 -1
- data/lib/wurk/job_util.rb +4 -1
- data/lib/wurk/launcher.rb +4 -3
- data/lib/wurk/lua.rb +1 -14
- data/lib/wurk/metrics/dashboard_series.rb +21 -2
- data/lib/wurk/metrics/history.rb +13 -5
- data/lib/wurk/metrics/query.rb +9 -2
- data/lib/wurk/metrics/statsd.rb +8 -0
- data/lib/wurk/middleware/chain.rb +85 -13
- data/lib/wurk/pool_checkout.rb +1 -1
- data/lib/wurk/queue_slot.rb +19 -9
- data/lib/wurk/rails_boot.rb +17 -6
- data/lib/wurk/railtie.rb +7 -3
- data/lib/wurk/redis_connection.rb +4 -2
- data/lib/wurk/unique.rb +11 -8
- data/lib/wurk/version.rb +1 -1
- data/lib/wurk/web/config.rb +42 -9
- data/lib/wurk/web/enterprise.rb +4 -3
- data/lib/wurk/web/extension.rb +74 -39
- data/lib/wurk.rb +5 -2
- data/vendor/assets/dashboard/assets/{ArgsValue-DsPb_KZ-.js → ArgsValue-DDqX6XWH.js} +1 -1
- data/vendor/assets/dashboard/assets/{BatchDetail-BCIXJOpZ.js → BatchDetail-lIytcn2C.js} +1 -1
- data/vendor/assets/dashboard/assets/{Batches-IjaQfXW6.js → Batches-BEumt-1O.js} +1 -1
- data/vendor/assets/dashboard/assets/{Busy-CaQrf1Ve.js → Busy-Vk9HmZcN.js} +1 -1
- data/vendor/assets/dashboard/assets/{Cron-B18EHn_Y.js → Cron-CWcM-Cfr.js} +1 -1
- data/vendor/assets/dashboard/assets/{Dashboard-DdT5ZaUg.js → Dashboard-Bgv1kusv.js} +1 -1
- data/vendor/assets/dashboard/assets/{Dead-DWIy-Mu8.js → Dead-Di5Rct2H.js} +1 -1
- data/vendor/assets/dashboard/assets/Extension-LZY5-imc.js +1 -0
- data/vendor/assets/dashboard/assets/FilterBox-BDo-7IAy.js +2 -0
- data/vendor/assets/dashboard/assets/{FlowDetail-DnexL72n.js → FlowDetail-CehwRLTt.js} +1 -1
- data/vendor/assets/dashboard/assets/{FlowState-Cr4aX6-d.js → FlowState-glK2Tnf8.js} +1 -1
- data/vendor/assets/dashboard/assets/{Flows-DO6EimbZ.js → Flows-cgy-Fsbp.js} +1 -1
- data/vendor/assets/dashboard/assets/{JobDetailModal-aPjgq-U9.js → JobDetailModal-Cuk3jruU.js} +1 -1
- data/vendor/assets/dashboard/assets/{Limiters-DxpwWCNe.js → Limiters-Ci8G2yZG.js} +1 -1
- data/vendor/assets/dashboard/assets/Metrics-CQFUeZTR.js +1 -0
- data/vendor/assets/dashboard/assets/{NotFound-BfP9uAV9.js → NotFound-CYjhuDsI.js} +1 -1
- data/vendor/assets/dashboard/assets/{PageHeader-DbC97afG.js → PageHeader-DjIAFTyX.js} +1 -1
- data/vendor/assets/dashboard/assets/{Profiles-BcaHreMe.js → Profiles-DTowqIRg.js} +1 -1
- data/vendor/assets/dashboard/assets/{Queues-ZlBc5tyD.js → Queues-DUGRPjME.js} +1 -1
- data/vendor/assets/dashboard/assets/{Retries-Bg3nkeiL.js → Retries-SJ3BaKC0.js} +1 -1
- data/vendor/assets/dashboard/assets/{Scheduled-BrMZPZGl.js → Scheduled-DxSRpmFZ.js} +1 -1
- data/vendor/assets/dashboard/assets/{Search-B5X5pGht.js → Search-bBbi5JNK.js} +1 -1
- data/vendor/assets/dashboard/assets/{charts-uDyf4PN0.js → charts-Bg2YBcaD.js} +1 -1
- data/vendor/assets/dashboard/assets/i18n-BZClP62Q.js +1 -0
- data/vendor/assets/dashboard/assets/index-BOTVSP9M.js +141 -0
- data/vendor/assets/dashboard/assets/index-Rewk4eSE.css +1 -0
- data/vendor/assets/dashboard/assets/{useResetPageOnEmpty-O3-5rMk3.js → useResetPageOnEmpty-BRPNyV2M.js} +1 -1
- data/vendor/assets/dashboard/assets/{useSort-xfnwjQAu.js → useSort-B-BLJik7.js} +1 -1
- data/vendor/assets/dashboard/assets/{utils-BIKHwNKB.js → utils-CGyrKpTh.js} +1 -1
- data/vendor/assets/dashboard/index.html +3 -3
- data/vendor/assets/dashboard/wurk-manifest.json +2 -2
- metadata +30 -44
- data/vendor/assets/dashboard/assets/Extension-BjBUWhHf.js +0 -1
- data/vendor/assets/dashboard/assets/FilterBox-DwH3PXXt.js +0 -1
- data/vendor/assets/dashboard/assets/Metrics-C3JL1GIH.js +0 -1
- data/vendor/assets/dashboard/assets/i18n-CJeYfeVt.js +0 -1
- data/vendor/assets/dashboard/assets/index-B36QCAf-.css +0 -1
- data/vendor/assets/dashboard/assets/index-DQujgZgH.js +0 -141
data/lib/wurk/metrics/statsd.rb
CHANGED
|
@@ -164,6 +164,14 @@ module Wurk
|
|
|
164
164
|
# into a JobRetry::Skip, and a job cut by its deadline passes through
|
|
165
165
|
# before Middleware::Expiry books it `expired` — both sit outside this
|
|
166
166
|
# middleware, and without this arm either one emits `jobs.failure`.
|
|
167
|
+
# Their registrations are
|
|
168
|
+
# `Wurk.configuration.server_middleware.prepend(Wurk::Middleware::InterruptHandler)`
|
|
169
|
+
# (lib/wurk/middleware/interrupt_handler.rb:50) and
|
|
170
|
+
# `Wurk.configuration.server_middleware.add(Wurk::Middleware::Expiry)`
|
|
171
|
+
# (lib/wurk/middleware/expiry.rb:111); this middleware is added at
|
|
172
|
+
# `Wurk.configuration.server_middleware.add(Wurk::Metrics::Statsd)`
|
|
173
|
+
# (lib/wurk.rb:351), after Limiter's add at lib/wurk.rb:335 and before
|
|
174
|
+
# History's add at lib/wurk.rb:358.
|
|
167
175
|
# Pro's statsd emitter is closed source, so the oracle is the free
|
|
168
176
|
# ExecutionTracker plus the rule that the two Wurk emitters must never
|
|
169
177
|
# classify one event differently. Signed off in
|
|
@@ -7,7 +7,13 @@ module Wurk
|
|
|
7
7
|
# pushes to index 0, `insert_before`/`insert_after` anchor relative to an
|
|
8
8
|
# existing entry, `invoke` walks the chain handing the inner block to each.
|
|
9
9
|
#
|
|
10
|
-
#
|
|
10
|
+
# `add` has one opt-in exception, declared per middleware class: a class
|
|
11
|
+
# may name the classes it must stay OUTSIDE of in a class-level `outer_of`,
|
|
12
|
+
# and `add` then places it before the first of those anchors instead of at
|
|
13
|
+
# the tail. A class that declares no anchor keeps the plain append.
|
|
14
|
+
#
|
|
15
|
+
# Entries store klass + ctor args + the config they were registered with,
|
|
16
|
+
# not the live object: every job gets a
|
|
11
17
|
# fresh instance per entry (`invoke` builds each as it reaches it, `retrieve`
|
|
12
18
|
# builds them all up front). This keeps middleware thread-safe by
|
|
13
19
|
# construction and matches Sidekiq's lifecycle.
|
|
@@ -19,20 +25,28 @@ module Wurk
|
|
|
19
25
|
attr_reader :entries
|
|
20
26
|
attr_accessor :config
|
|
21
27
|
|
|
22
|
-
def initialize(config = nil)
|
|
28
|
+
def initialize(config = nil, on_registration: nil)
|
|
23
29
|
@config = config
|
|
30
|
+
@on_registration = on_registration
|
|
24
31
|
@entries = []
|
|
25
32
|
yield self if block_given?
|
|
26
33
|
end
|
|
27
34
|
|
|
28
35
|
def each(&) = @entries.each(&)
|
|
29
36
|
|
|
30
|
-
# Bind a duplicate of this chain to `capsule`.
|
|
31
|
-
# config
|
|
32
|
-
# the
|
|
37
|
+
# Bind a duplicate of this chain to `capsule`. Entries carry their
|
|
38
|
+
# config, so each is duplicated and only the duplicate rebound: the
|
|
39
|
+
# capsule's middleware resolve the capsule while the parent's entries
|
|
40
|
+
# keep the parent config. Array#dup on `@entries` keeps the two chains
|
|
41
|
+
# independent after the first `add`/`remove`/etc.
|
|
33
42
|
def copy_for(capsule)
|
|
34
43
|
copy = dup
|
|
35
44
|
copy.instance_variable_set(:@config, capsule)
|
|
45
|
+
copy.entries.map! do |entry|
|
|
46
|
+
rebound = entry.dup
|
|
47
|
+
rebound.config = capsule
|
|
48
|
+
rebound
|
|
49
|
+
end
|
|
36
50
|
copy
|
|
37
51
|
end
|
|
38
52
|
|
|
@@ -40,26 +54,65 @@ module Wurk
|
|
|
40
54
|
@entries.delete_if { |entry| entry.klass == klass }
|
|
41
55
|
end
|
|
42
56
|
|
|
57
|
+
# Appends the middleware — the documented Sidekiq behaviour (spec
|
|
58
|
+
# §10.1): any existing entry for `klass` is removed and the new one
|
|
59
|
+
# lands at the tail (innermost).
|
|
60
|
+
#
|
|
61
|
+
# One opt-in exception, declared per class: a middleware may name the
|
|
62
|
+
# classes it must stay OUTSIDE of in a class-level `outer_of`. When one
|
|
63
|
+
# of those anchors is registered, the entry is placed before the first
|
|
64
|
+
# of them instead of at the tail — so re-registering such a middleware
|
|
65
|
+
# cannot silently move it inside the middleware it depends on being
|
|
66
|
+
# outside of. A class that declares no anchor keeps the plain append.
|
|
67
|
+
#
|
|
68
|
+
# An entry already registered before the anchor keeps its slot (only
|
|
69
|
+
# its ctor args are refreshed): moving it would be a relocation the
|
|
70
|
+
# caller did not ask for.
|
|
43
71
|
def add(klass, *)
|
|
72
|
+
@on_registration&.call(klass)
|
|
73
|
+
entry = make_entry(klass, *)
|
|
74
|
+
anchors = anchor_classes(klass)
|
|
75
|
+
existing = @entries.index { |e| e.klass == klass }
|
|
76
|
+
anchor = @entries.index { |e| anchors.include?(e.klass) }
|
|
77
|
+
|
|
78
|
+
# No anchor registered: remove the old entry, append at the tail.
|
|
79
|
+
if anchor.nil?
|
|
80
|
+
remove(klass)
|
|
81
|
+
@entries << entry
|
|
82
|
+
return @entries
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Already outside the anchor: keep the slot, refresh the entry.
|
|
86
|
+
if existing && existing < anchor
|
|
87
|
+
@entries[existing] = entry
|
|
88
|
+
return @entries
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Registered inside the anchor (or not registered at all): move it
|
|
92
|
+
# out, to just before the first anchor.
|
|
44
93
|
remove(klass)
|
|
45
|
-
@entries
|
|
94
|
+
@entries.insert(anchor, entry)
|
|
95
|
+
@entries
|
|
46
96
|
end
|
|
47
97
|
|
|
48
98
|
def prepend(klass, *)
|
|
99
|
+
@on_registration&.call(klass)
|
|
49
100
|
remove(klass)
|
|
50
|
-
@entries.unshift(
|
|
101
|
+
@entries.unshift(make_entry(klass, *))
|
|
51
102
|
end
|
|
52
103
|
|
|
53
104
|
def insert_before(oldklass, newklass, *)
|
|
105
|
+
@on_registration&.call(newklass)
|
|
54
106
|
i = @entries.index { |entry| entry.klass == newklass }
|
|
55
|
-
new_entry = i.nil? ?
|
|
107
|
+
new_entry = i.nil? ? make_entry(newklass, *) : @entries.delete_at(i)
|
|
56
108
|
i = @entries.index { |entry| entry.klass == oldklass } || 0
|
|
57
109
|
@entries.insert(i, new_entry)
|
|
58
110
|
end
|
|
59
111
|
|
|
60
112
|
def insert_after(oldklass, newklass, *)
|
|
113
|
+
@on_registration&.call(newklass)
|
|
61
114
|
i = @entries.index { |entry| entry.klass == newklass }
|
|
62
|
-
new_entry = i.nil? ?
|
|
115
|
+
new_entry = i.nil? ? make_entry(newklass, *) : @entries.delete_at(i)
|
|
63
116
|
i = @entries.index { |entry| entry.klass == oldklass } || (@entries.size - 1)
|
|
64
117
|
@entries.insert(i + 1, new_entry)
|
|
65
118
|
end
|
|
@@ -74,7 +127,7 @@ module Wurk
|
|
|
74
127
|
end
|
|
75
128
|
|
|
76
129
|
def retrieve
|
|
77
|
-
map
|
|
130
|
+
map(&:make_new)
|
|
78
131
|
end
|
|
79
132
|
|
|
80
133
|
def clear
|
|
@@ -101,7 +154,7 @@ module Wurk
|
|
|
101
154
|
def traverse(entries, index, args, &block)
|
|
102
155
|
return yield if index == entries.size
|
|
103
156
|
|
|
104
|
-
entries[index].make_new
|
|
157
|
+
entries[index].make_new.call(*args) do
|
|
105
158
|
traverse(entries, index + 1, args, &block)
|
|
106
159
|
end
|
|
107
160
|
end
|
|
@@ -111,6 +164,24 @@ module Wurk
|
|
|
111
164
|
def initialize_copy(orig)
|
|
112
165
|
super
|
|
113
166
|
@entries = orig.entries.dup
|
|
167
|
+
@on_registration = nil
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# The ordering anchors a middleware class declares, if any: a class-level
|
|
171
|
+
# `outer_of` returning the classes it must stay OUTSIDE of. Empty when
|
|
172
|
+
# the class declares none — the plain Sidekiq append.
|
|
173
|
+
def anchor_classes(klass)
|
|
174
|
+
return [] unless klass.respond_to?(:outer_of)
|
|
175
|
+
|
|
176
|
+
Array(klass.outer_of)
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# Registration binds the entry to this chain's config/capsule so that
|
|
180
|
+
# `Entry#make_new` can stay zero-argument (spec §10.1).
|
|
181
|
+
def make_entry(klass, *)
|
|
182
|
+
entry = Entry.new(klass, *)
|
|
183
|
+
entry.config = @config
|
|
184
|
+
entry
|
|
114
185
|
end
|
|
115
186
|
|
|
116
187
|
# Holds the klass + ctor args for a registered middleware. Defers
|
|
@@ -127,6 +198,7 @@ module Wurk
|
|
|
127
198
|
# a class that does not declare it still gets asked.
|
|
128
199
|
class Entry
|
|
129
200
|
attr_reader :klass
|
|
201
|
+
attr_accessor :config
|
|
130
202
|
|
|
131
203
|
def initialize(klass, *args)
|
|
132
204
|
@klass = klass
|
|
@@ -134,9 +206,9 @@ module Wurk
|
|
|
134
206
|
@takes_config = klass.public_method_defined?(:config=)
|
|
135
207
|
end
|
|
136
208
|
|
|
137
|
-
def make_new
|
|
209
|
+
def make_new
|
|
138
210
|
instance = @klass.new(*@args)
|
|
139
|
-
instance.config = config if
|
|
211
|
+
instance.config = config if @takes_config || instance.respond_to?(:config=)
|
|
140
212
|
instance
|
|
141
213
|
end
|
|
142
214
|
end
|
data/lib/wurk/pool_checkout.rb
CHANGED
|
@@ -9,7 +9,7 @@ module Wurk
|
|
|
9
9
|
# after a command may already have applied server-side (see RedisPool#with).
|
|
10
10
|
# The object behind a wrapper is not always ours, though: the Sidekiq surface
|
|
11
11
|
# Wurk drops into lets a host supply any ConnectionPool-shaped object
|
|
12
|
-
# (`Sidekiq::Client#redis_pool=`,
|
|
12
|
+
# (`Sidekiq::Client#redis_pool=`, the `pool:`
|
|
13
13
|
# kwarg on Stats::History / Deploy / Leader / Profiler / Metrics::History),
|
|
14
14
|
# and a bare `def with; yield conn; end` decorator — the shape people reach
|
|
15
15
|
# for to count or trace round trips — is a legal implementation of it. Handing
|
data/lib/wurk/queue_slot.rb
CHANGED
|
@@ -65,15 +65,25 @@ module Wurk
|
|
|
65
65
|
# of, and it is why a live holder has to keep saying so — {HELD} is the
|
|
66
66
|
# ledger the heartbeat refreshes from (see {refresh_in}).
|
|
67
67
|
#
|
|
68
|
-
# A rolling restart
|
|
69
|
-
#
|
|
70
|
-
#
|
|
71
|
-
#
|
|
72
|
-
#
|
|
73
|
-
#
|
|
74
|
-
#
|
|
75
|
-
#
|
|
76
|
-
#
|
|
68
|
+
# A rolling restart does not run a queue over its cap. `Swarm::Restart` boots
|
|
69
|
+
# the replacement before it TERMs the old child, but both generations' holds
|
|
70
|
+
# are members of the one ZSET and `fetch_slot.lua` refuses a fresh claim once
|
|
71
|
+
# its ZCARD reaches capacity — so the overlap throttles the replacement. The
|
|
72
|
+
# ledger stays honest through the drain: a quieted child keeps beating
|
|
73
|
+
# (Launcher#quiet deliberately leaves the heartbeat running), so its holds
|
|
74
|
+
# stay refreshed until each job's own release takes it off. The one hold
|
|
75
|
+
# that leaves while its job is still in flight is the drain-deadline
|
|
76
|
+
# hand-back — a drain that overruns its deadline bulk-requeues the payload
|
|
77
|
+
# and raises `Wurk::Shutdown`, and the unwind's `ensure` reaches
|
|
78
|
+
# `UnitOfWork#release_slot` while the job is still unwinding. That is a
|
|
79
|
+
# slot-for-slot hand-back, not an extra admission: the re-run spends the
|
|
80
|
+
# freed capacity on its own fresh admission. A child SIGKILLed instead of
|
|
81
|
+
# draining releases nothing, so its holds stay counted until the TTL
|
|
82
|
+
# reclaims them — a refusal window, at most one `drain_timeout` or
|
|
83
|
+
# {TTL_SECONDS} long, never an over-run. What remains deliberate: booting
|
|
84
|
+
# the replacement warm instead of starving it behind the old child's last
|
|
85
|
+
# job, and never handing capacity between processes — that would need the
|
|
86
|
+
# counter this ledger exists to avoid.
|
|
77
87
|
module QueueSlot
|
|
78
88
|
# How long a hold survives with nobody refreshing it. Deliberately the same
|
|
79
89
|
# as `Heartbeat::TTL_SECONDS` (pinned by a test): a hold is refreshed on the
|
data/lib/wurk/rails_boot.rb
CHANGED
|
@@ -18,9 +18,10 @@ module Wurk
|
|
|
18
18
|
# Invoked from the `wurk.server_mode` initializer, before config/initializers
|
|
19
19
|
# load. This Rails process forks the workers, so it IS the server. Enter
|
|
20
20
|
# server mode now — otherwise the app's `Sidekiq.configure_server` blocks
|
|
21
|
-
# gate on `config.server?` (still false) and are
|
|
22
|
-
#
|
|
23
|
-
#
|
|
21
|
+
# gate on `config.server?` (still false) and are dropped, each skip logging
|
|
22
|
+
# one WARN naming the gate (`configure_server` / `server? is false`). A
|
|
23
|
+
# process that won't run workers (skip_boot?, or one that refuses to boot
|
|
24
|
+
# under a preforking web server) is not a server.
|
|
24
25
|
#
|
|
25
26
|
# It is a client as well: its web requests enqueue, so the app's
|
|
26
27
|
# `Sidekiq.configure_client` blocks (client middleware, the client's Redis)
|
|
@@ -68,17 +69,27 @@ module Wurk
|
|
|
68
69
|
# custom script that requires config/environment — and a denylist can
|
|
69
70
|
# never name them all: each one forked a full swarm that fetched jobs, and
|
|
70
71
|
# its at_exit drain then cut those jobs off when the one-off command
|
|
71
|
-
# finished. The markers are constants only a running server defines
|
|
72
|
-
# (Bundler.require of the puma gem loads `Puma`, never `Puma::Launcher`).
|
|
72
|
+
# finished. The markers are constants only a running server defines.
|
|
73
73
|
# Any other server (Falcon, Thin, Pitchfork, a bespoke rackup) opts in
|
|
74
74
|
# with WURK_EMBED=1.
|
|
75
75
|
def serving?
|
|
76
76
|
return true if ENV['WURK_EMBED'] == '1'
|
|
77
|
-
return true if defined?(::Rails::Server) ||
|
|
77
|
+
return true if defined?(::Rails::Server) || puma_launcher_loaded? || defined?(::PhusionPassenger)
|
|
78
78
|
|
|
79
79
|
unicorn_server?
|
|
80
80
|
end
|
|
81
81
|
|
|
82
|
+
# Bundler.require of the puma gem defines `Puma` in every process, and
|
|
83
|
+
# puma >= 6 registers `autoload :Launcher` in lib/puma.rb — `defined?`
|
|
84
|
+
# reports an autoload as "constant" without loading it, so the bare check
|
|
85
|
+
# was true in `rails runner` / a console and forked a swarm there. Only a
|
|
86
|
+
# running server has actually loaded the launcher.
|
|
87
|
+
def puma_launcher_loaded?
|
|
88
|
+
return false unless defined?(::Puma::Launcher)
|
|
89
|
+
|
|
90
|
+
!::Puma.autoload?(:Launcher)
|
|
91
|
+
end
|
|
92
|
+
|
|
82
93
|
# `require 'unicorn'` (a Gemfile entry) defines the constant in every
|
|
83
94
|
# process, so the program name is what tells the server apart.
|
|
84
95
|
def unicorn_server?
|
data/lib/wurk/railtie.rb
CHANGED
|
@@ -18,11 +18,15 @@ module Wurk
|
|
|
18
18
|
# @@options, so `Rails.application.config.wurk` reads back the same object.
|
|
19
19
|
config.wurk = ::ActiveSupport::OrderedOptions.new
|
|
20
20
|
|
|
21
|
-
#
|
|
22
|
-
#
|
|
23
|
-
# and are silently dropped. RailsBoot decides whether this process serves.
|
|
21
|
+
# The gemspec ships no .rake files, so the railtie loads Wurk's
|
|
22
|
+
# host-facing rake tasks itself for `bin/rails wurk:import:cron` to exist.
|
|
24
23
|
rake_tasks { load File.expand_path('rake_tasks.rb', __dir__) }
|
|
25
24
|
|
|
25
|
+
# Enter server mode BEFORE config/initializers load — otherwise the app's
|
|
26
|
+
# `Sidekiq.configure_server` blocks gate on `config.server?` (still false)
|
|
27
|
+
# and are dropped, each skip logging one WARN naming the gate
|
|
28
|
+
# (`configure_server` / `server? is false`). RailsBoot decides whether this
|
|
29
|
+
# process serves.
|
|
26
30
|
initializer 'wurk.server_mode', before: :load_config_initializers do |app|
|
|
27
31
|
Wurk::RailsBoot.enter_server_mode_if_serving(app)
|
|
28
32
|
end
|
|
@@ -5,11 +5,13 @@ require_relative 'redis_pool'
|
|
|
5
5
|
module Wurk
|
|
6
6
|
# Sidekiq-compatible pool constructor. `Sidekiq::RedisConnection.create(...)`
|
|
7
7
|
# is the documented way to build a standalone Redis pool (spec §26) — it's the
|
|
8
|
-
# default pool for `Sidekiq::Deploy` (§23)
|
|
9
|
-
# pools in multi-shard web mounts (Pro §10.2). Returns a `Wurk::RedisPool`
|
|
8
|
+
# default pool for `Sidekiq::Deploy` (§23). Returns a `Wurk::RedisPool`
|
|
10
9
|
# (connection_pool-backed, redis-client adapter), which is `.with`-compatible
|
|
11
10
|
# with everything that expects a Sidekiq pool.
|
|
12
11
|
#
|
|
12
|
+
# Pro §10.2 pool-bound multi-shard web mounts (`Sidekiq::Pro::Web.with`) are
|
|
13
|
+
# unsupported: `Wurk::Web` has no `.with` factory.
|
|
14
|
+
#
|
|
13
15
|
# Accepts Sidekiq's option keys (`url`, `size`, `pool_timeout`, `pool_name`,
|
|
14
16
|
# and `name` — the Sentinel master next to `sentinels:`) plus the socket
|
|
15
17
|
# knobs (`connect_timeout`/`read_timeout`/`write_timeout`/
|
data/lib/wurk/unique.rb
CHANGED
|
@@ -98,9 +98,12 @@ module Wurk
|
|
|
98
98
|
@enabled == true
|
|
99
99
|
end
|
|
100
100
|
|
|
101
|
-
|
|
101
|
+
# `config` selects the chains and death handlers, installed once each;
|
|
102
|
+
# the enabled flag remains process-global, including for other configs
|
|
103
|
+
# whose chains already carry the middleware.
|
|
104
|
+
def enable!(config = Wurk.configuration)
|
|
102
105
|
@enabled = true
|
|
103
|
-
register_middleware!
|
|
106
|
+
register_middleware!(config)
|
|
104
107
|
true
|
|
105
108
|
end
|
|
106
109
|
|
|
@@ -188,12 +191,12 @@ module Wurk
|
|
|
188
191
|
nil
|
|
189
192
|
end
|
|
190
193
|
|
|
191
|
-
def register_middleware!
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
handlers =
|
|
194
|
+
def register_middleware!(config)
|
|
195
|
+
client_chain = config.client_middleware
|
|
196
|
+
server_chain = config.server_middleware
|
|
197
|
+
client_chain.add(ClientMiddleware) unless client_chain.exists?(ClientMiddleware)
|
|
198
|
+
server_chain.add(ServerMiddleware) unless server_chain.exists?(ServerMiddleware)
|
|
199
|
+
handlers = config.death_handlers
|
|
197
200
|
handlers << DEATH_HANDLER unless handlers.include?(DEATH_HANDLER)
|
|
198
201
|
end
|
|
199
202
|
end
|
data/lib/wurk/version.rb
CHANGED
data/lib/wurk/web/config.rb
CHANGED
|
@@ -129,7 +129,9 @@ module Wurk
|
|
|
129
129
|
# (the default) out of localStorage; this is what it falls back to for a
|
|
130
130
|
# visitor who has never picked one. Injected ahead of the shell's
|
|
131
131
|
# pre-paint script so a host default paints with everything else instead
|
|
132
|
-
# of flashing the other palette. nil emits nothing.
|
|
132
|
+
# of flashing the other palette. nil emits nothing. Initialization and
|
|
133
|
+
# reset! sample WURK_WEB_DEFAULT_THEME through the validating writer;
|
|
134
|
+
# later explicit assignments (including nil) override that seed.
|
|
133
135
|
attr_reader :default_theme
|
|
134
136
|
|
|
135
137
|
def default_theme=(value)
|
|
@@ -163,7 +165,16 @@ module Wurk
|
|
|
163
165
|
# Keyed rather than flat because the negotiated hint is not the last
|
|
164
166
|
# word — a `?locale=` link or a stored pick outranks it — so the entry
|
|
165
167
|
# has to be chosen in the browser, against the locale actually rendered.
|
|
166
|
-
|
|
168
|
+
attr_reader :translations
|
|
169
|
+
|
|
170
|
+
def translations=(value)
|
|
171
|
+
raise ArgumentError, "translations must be a Hash (got #{value.class})" unless value.is_a?(::Hash)
|
|
172
|
+
|
|
173
|
+
value.each do |locale, copy|
|
|
174
|
+
validate_translation_hash!(copy, "translations[#{locale.inspect}]")
|
|
175
|
+
end
|
|
176
|
+
@translations = value
|
|
177
|
+
end
|
|
167
178
|
|
|
168
179
|
# Registers a `(env, method, path) -> truthy/falsey` block. Re-calling
|
|
169
180
|
# overwrites; the spec exposes a single hook, not a chain.
|
|
@@ -250,8 +261,8 @@ module Wurk
|
|
|
250
261
|
# that wraps the dashboard, in front of the authorization hook, so a
|
|
251
262
|
# host app can gate the UI with Devise/Warden/Sorcery/Rack::Auth::Basic
|
|
252
263
|
# without writing its own middleware. `args` and an optional block pass
|
|
253
|
-
# straight through to the middleware's `new`.
|
|
254
|
-
#
|
|
264
|
+
# straight through to the middleware's `new`. Each inner app's chain is
|
|
265
|
+
# cached until the middleware configuration changes.
|
|
255
266
|
def use(middleware, *args, &block)
|
|
256
267
|
@middlewares << [middleware, args, block]
|
|
257
268
|
end
|
|
@@ -263,13 +274,15 @@ module Wurk
|
|
|
263
274
|
# on one evict the other's chain. The snapshot matters because
|
|
264
275
|
# `middlewares` is the live array (upstream surface): direct mutation
|
|
265
276
|
# after the first request triggers a rebuild instead of silently serving
|
|
266
|
-
# the stale chain.
|
|
267
|
-
#
|
|
277
|
+
# the stale chain. Copy triples and positional-argument lists so edits
|
|
278
|
+
# cannot mutate the snapshot too; argument objects and blocks retain
|
|
279
|
+
# their identities (their internal mutations are not tracked). Steady
|
|
280
|
+
# state is one Hash lookup plus an == over a handful of entries.
|
|
268
281
|
def rack_app(inner)
|
|
269
282
|
snapshot, app = @rack_apps[inner]
|
|
270
283
|
return app if app && snapshot == @middlewares
|
|
271
284
|
|
|
272
|
-
stack =
|
|
285
|
+
stack = middleware_snapshot
|
|
273
286
|
app = ::Rack::Builder.new do
|
|
274
287
|
stack.each { |middleware, args, block| use(middleware, *args, &block) }
|
|
275
288
|
run inner
|
|
@@ -279,7 +292,7 @@ module Wurk
|
|
|
279
292
|
end
|
|
280
293
|
|
|
281
294
|
# Read-only mode. When on, the Authorization middleware blocks every
|
|
282
|
-
# non-GET request (retry/kill/
|
|
295
|
+
# non-GET request (retry/kill/add_to_queue/delete/pause/unpause/clear) with
|
|
283
296
|
# 403, and the SPA hides destructive actions via the /api/meta flag.
|
|
284
297
|
# Defaults from WURK_WEB_READ_ONLY=1 so a viewer-only deploy (e.g. the
|
|
285
298
|
# public demo) needs no Ruby config.
|
|
@@ -296,7 +309,7 @@ module Wurk
|
|
|
296
309
|
@authorization = nil
|
|
297
310
|
@read_only = env_read_only?
|
|
298
311
|
@read_only_message = nil
|
|
299
|
-
|
|
312
|
+
self.default_theme = ENV.fetch('WURK_WEB_DEFAULT_THEME', nil)
|
|
300
313
|
@max_streams = nil
|
|
301
314
|
@middlewares = []
|
|
302
315
|
@rack_apps = {}.compare_by_identity
|
|
@@ -316,6 +329,26 @@ module Wurk
|
|
|
316
329
|
|
|
317
330
|
private
|
|
318
331
|
|
|
332
|
+
def middleware_snapshot
|
|
333
|
+
@middlewares.map { |middleware, args, block| [middleware, args.dup, block] }
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
def validate_translation_hash!(copy, path)
|
|
337
|
+
raise ArgumentError, "#{path} must be a Hash (got #{copy.class})" unless copy.is_a?(::Hash)
|
|
338
|
+
|
|
339
|
+
copy.each do |key, value|
|
|
340
|
+
child_path = "#{path}[#{key.inspect}]"
|
|
341
|
+
case value
|
|
342
|
+
when ::Hash
|
|
343
|
+
validate_translation_hash!(value, child_path)
|
|
344
|
+
when ::String
|
|
345
|
+
next
|
|
346
|
+
else
|
|
347
|
+
raise ArgumentError, "#{child_path} must be a String or Hash (got #{value.class})"
|
|
348
|
+
end
|
|
349
|
+
end
|
|
350
|
+
end
|
|
351
|
+
|
|
319
352
|
def env_read_only?
|
|
320
353
|
ENV['WURK_WEB_READ_ONLY'] == '1'
|
|
321
354
|
end
|
data/lib/wurk/web/enterprise.rb
CHANGED
|
@@ -198,9 +198,10 @@ module Wurk
|
|
|
198
198
|
end
|
|
199
199
|
|
|
200
200
|
# Global top-N rollup. Wraps `Query.top_jobs` and keeps the wire
|
|
201
|
-
# shape of `[[class_name, {p:, f:, ms:}], ...]`.
|
|
202
|
-
|
|
203
|
-
|
|
201
|
+
# shape of `[[class_name, {p:, f:, ms:}], ...]`. `hours:` reads the
|
|
202
|
+
# 10-minute buckets instead of the per-minute ones.
|
|
203
|
+
def top(minutes: 60, hours: nil, class_filter: nil)
|
|
204
|
+
Wurk::Metrics::Query.top_jobs(minutes: minutes, hours: hours, class_filter: class_filter)
|
|
204
205
|
end
|
|
205
206
|
|
|
206
207
|
# Cluster-total time-series for the dashboard charts. `bucket` is
|