joblin 0.1.10 → 0.1.11
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/lib/joblin/batching/compat/sidekiq/web.rb +171 -102
- data/lib/joblin/batching/compat/sidekiq.rb +12 -1
- data/lib/joblin/batching/pool.rb +0 -0
- data/lib/joblin/uniqueness/compat/sidekiq.rb +12 -1
- data/lib/joblin/version.rb +1 -1
- data/spec/batching/compat/sidekiq_spec.rb +30 -0
- data/spec/batching/compat/sidekiq_web_spec.rb +185 -0
- data/spec/internal/log/test.log +8546 -0
- data/spec/uniqueness/compat/sidekiq_spec.rb +30 -0
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: fee4e742fd313bae02c2d077066985c147749f207dfd211ac5adb29426a80d84
|
|
4
|
+
data.tar.gz: d0d074e5a51d65ee15a63e23a549240014f2d09c882ce5632163ab4a9a444c7a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8f91c842abc953c54b356d4fb58e69bfda58e30684ede90b71a7e5503d54c198a0ae823e5e84ee81c94d5af92d8260bb331deb5f4270a09313c695d8d2afbd99
|
|
7
|
+
data.tar.gz: 8a3c1553aff7ac339d16e30931e5960aaee92047b4424b9a59e6b8d4f1ea6d38a5a8e3c8a3ad2aa999bc36dd694ff80ee579013c0047066de03d62baab24637a
|
|
@@ -10,41 +10,131 @@ require_relative "web/helpers"
|
|
|
10
10
|
module Joblin::Batching::Compat::Sidekiq
|
|
11
11
|
module Web
|
|
12
12
|
DEV_MODE = (defined?(Rails) && !Rails.env.production?) || !!ENV["SIDEKIQ_WEB_TESTING"]
|
|
13
|
-
::Sidekiq::WebHelpers::SAFE_QPARAMS << 'all_batches'
|
|
14
|
-
::Sidekiq::WebHelpers::SAFE_QPARAMS << 'count'
|
|
15
13
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
14
|
+
if defined?(::Sidekiq::WebHelpers::SAFE_QPARAMS)
|
|
15
|
+
::Sidekiq::WebHelpers::SAFE_QPARAMS << 'all_batches' unless ::Sidekiq::WebHelpers::SAFE_QPARAMS.include?('all_batches')
|
|
16
|
+
::Sidekiq::WebHelpers::SAFE_QPARAMS << 'count' unless ::Sidekiq::WebHelpers::SAFE_QPARAMS.include?('count')
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Helpers shared with the route blocks. We expose these via a module so the
|
|
20
|
+
# same registration code works on both Sidekiq 7 (which accepts a block) and
|
|
21
|
+
# Sidekiq 8 (whose `Application.helpers` only accepts a module).
|
|
22
|
+
module RouteHelpers
|
|
23
|
+
include Web::Helpers
|
|
24
|
+
|
|
25
|
+
def dev_mode?
|
|
26
|
+
DEV_MODE
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Read a route (path) parameter such as `:bid` from `/batches/:bid`.
|
|
30
|
+
#
|
|
31
|
+
# We cannot rely on `params[:bid]` across majors: Sidekiq 7's `params` is
|
|
32
|
+
# an indifferent hash that includes route params, but Sidekiq 8's `params`
|
|
33
|
+
# returns only the query string (String keys), so `params[:bid]` would be
|
|
34
|
+
# nil there. Sidekiq 8 exposes route params via `route_params(:bid)`. We
|
|
35
|
+
# read straight from the Rack env, which the router populates under this
|
|
36
|
+
# key on both Sidekiq 7 (WebRouter::ROUTE_PARAMS) and Sidekiq 8, so the
|
|
37
|
+
# same call works regardless of version and avoids deprecation warnings.
|
|
38
|
+
def route_param(key)
|
|
39
|
+
(env["rack.route_params"] || {})[key.to_sym]
|
|
40
|
+
end
|
|
19
41
|
|
|
20
|
-
|
|
21
|
-
|
|
42
|
+
def tree_data(root_bid, slice: nil)
|
|
43
|
+
tree_bids = Joblin::Batching::Batch.bid_hierarchy(root_bid, slice: slice)
|
|
44
|
+
|
|
45
|
+
Joblin::Batching::Batch.redis do |r|
|
|
46
|
+
layer_data = ->(layer, parent = nil) {
|
|
47
|
+
bid = layer[0]
|
|
48
|
+
batch = Joblin::Batching::Batch.new(bid)
|
|
49
|
+
|
|
50
|
+
jobs_total = r.hget("BID-#{bid}", "job_count").to_i
|
|
51
|
+
jobs_pending = r.hget("BID-#{bid}", 'pending').to_i
|
|
52
|
+
jobs_failed = r.scard("BID-#{bid}-failed").to_i
|
|
53
|
+
jobs_dead = r.scard("BID-#{bid}-dead").to_i
|
|
54
|
+
jobs_success = jobs_total - jobs_pending
|
|
55
|
+
|
|
56
|
+
batches_total = r.hget("BID-#{bid}", 'children').to_i
|
|
57
|
+
batches_success = r.scard("BID-#{bid}-batches-success").to_i
|
|
58
|
+
batches_pending = batches_total - batches_success
|
|
59
|
+
batches_failed = r.scard("BID-#{bid}-batches-failed").to_i
|
|
60
|
+
|
|
61
|
+
status = 'in_progress'
|
|
62
|
+
status = 'complete' if batches_pending == batches_failed && jobs_pending == jobs_failed
|
|
63
|
+
status = 'success' if batches_pending == 0 && jobs_pending == 0
|
|
64
|
+
status = 'deleted' if bid != root_bid && !batch.parent_bid
|
|
65
|
+
|
|
66
|
+
{
|
|
67
|
+
bid: bid,
|
|
68
|
+
created_at: r.hget("BID-#{bid}", 'created_at'),
|
|
69
|
+
status: status,
|
|
70
|
+
parent_bid: parent ? parent.bid : batch.parent_bid,
|
|
71
|
+
description: batch.description,
|
|
72
|
+
jobs: {
|
|
73
|
+
pending_count: jobs_pending,
|
|
74
|
+
successful_count: jobs_success,
|
|
75
|
+
failed_count: jobs_failed,
|
|
76
|
+
dead_count: jobs_dead,
|
|
77
|
+
total_count: jobs_total,
|
|
78
|
+
# items: batches.map{|b| layer_data[b] },
|
|
79
|
+
},
|
|
80
|
+
batches: {
|
|
81
|
+
pending_count: batches_pending,
|
|
82
|
+
successful_count: batches_success,
|
|
83
|
+
failed_count: batches_failed,
|
|
84
|
+
total_count: batches_total,
|
|
85
|
+
items: layer[1].map{|b| layer_data[b, batch] },
|
|
86
|
+
},
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
data = layer_data[tree_bids]
|
|
91
|
+
data[:batches][:slice] = slice if slice
|
|
92
|
+
data
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def format_context(batch)
|
|
97
|
+
bits = []
|
|
98
|
+
own_keys = batch.context.own.keys
|
|
99
|
+
batch.context.flatten.each do |k,v|
|
|
100
|
+
added = own_keys.include? k
|
|
101
|
+
bits << " <span class=\"key #{added ? 'own' : 'inherited'}\">\"#{k}\": #{v.to_json},</span>"
|
|
22
102
|
end
|
|
103
|
+
bits = [
|
|
104
|
+
"{ // <span class=\"own\">Added</span> / <span class=\"inherited\">Inherited</span>",
|
|
105
|
+
*bits,
|
|
106
|
+
'}'
|
|
107
|
+
]
|
|
108
|
+
bits.join("\n")
|
|
23
109
|
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def self.registered(app) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
|
|
113
|
+
app.helpers(RouteHelpers)
|
|
24
114
|
|
|
25
115
|
# =============== BATCHES =============== #
|
|
26
116
|
|
|
27
117
|
app.get "/batches" do
|
|
28
|
-
@count = (
|
|
118
|
+
@count = (url_params('count') || 25).to_i
|
|
29
119
|
|
|
30
|
-
source_key =
|
|
31
|
-
@current_page, @total_size, @batches = page(source_key,
|
|
120
|
+
source_key = url_params('all_batches') ? "batches" : "BID-ROOT-bids"
|
|
121
|
+
@current_page, @total_size, @batches = page(source_key, url_params('page'), @count)
|
|
32
122
|
@batches = @batches.map {|b, score| Joblin::Batching::Batch.new(b) }
|
|
33
123
|
|
|
34
124
|
erb(get_template(:batches))
|
|
35
125
|
end
|
|
36
126
|
|
|
37
127
|
app.get "/batches/:bid" do
|
|
38
|
-
@bid =
|
|
128
|
+
@bid = route_param(:bid)
|
|
39
129
|
@batch = Joblin::Batching::Batch.new(@bid)
|
|
40
130
|
|
|
41
131
|
@tree_data = tree_data(@bid)
|
|
42
132
|
|
|
43
|
-
@count = (
|
|
44
|
-
@current_batches_page, @total_batches_size, @sub_batches = page("BID-#{@batch.bid}-bids",
|
|
133
|
+
@count = (url_params('count') || 25).to_i
|
|
134
|
+
@current_batches_page, @total_batches_size, @sub_batches = page("BID-#{@batch.bid}-bids", url_params('batch_page'), @count)
|
|
45
135
|
@sub_batches = @sub_batches.map {|b, score| Joblin::Batching::Batch.new(b) }
|
|
46
136
|
|
|
47
|
-
@current_jobs_page, @total_jobs_size, @jobs = page("BID-#{@batch.bid}-jids",
|
|
137
|
+
@current_jobs_page, @total_jobs_size, @jobs = page("BID-#{@batch.bid}-jids", url_params('job_page'), @count)
|
|
48
138
|
@jobs = @jobs.map do |jid, score|
|
|
49
139
|
{ jid: jid, }
|
|
50
140
|
end
|
|
@@ -53,84 +143,13 @@ module Joblin::Batching::Compat::Sidekiq
|
|
|
53
143
|
end
|
|
54
144
|
|
|
55
145
|
app.get "/batches/:bid/tree" do
|
|
56
|
-
@bid =
|
|
146
|
+
@bid = route_param(:bid)
|
|
57
147
|
|
|
58
|
-
json(tree_data(@bid, slice:
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
app.helpers do
|
|
62
|
-
def tree_data(root_bid, slice: nil)
|
|
63
|
-
tree_bids = Joblin::Batching::Batch.bid_hierarchy(root_bid, slice: slice)
|
|
64
|
-
|
|
65
|
-
Joblin::Batching::Batch.redis do |r|
|
|
66
|
-
layer_data = ->(layer, parent = nil) {
|
|
67
|
-
bid = layer[0]
|
|
68
|
-
batch = Joblin::Batching::Batch.new(bid)
|
|
69
|
-
|
|
70
|
-
jobs_total = r.hget("BID-#{bid}", "job_count").to_i
|
|
71
|
-
jobs_pending = r.hget("BID-#{bid}", 'pending').to_i
|
|
72
|
-
jobs_failed = r.scard("BID-#{bid}-failed").to_i
|
|
73
|
-
jobs_dead = r.scard("BID-#{bid}-dead").to_i
|
|
74
|
-
jobs_success = jobs_total - jobs_pending
|
|
75
|
-
|
|
76
|
-
batches_total = r.hget("BID-#{bid}", 'children').to_i
|
|
77
|
-
batches_success = r.scard("BID-#{bid}-batches-success").to_i
|
|
78
|
-
batches_pending = batches_total - batches_success
|
|
79
|
-
batches_failed = r.scard("BID-#{bid}-batches-failed").to_i
|
|
80
|
-
|
|
81
|
-
status = 'in_progress'
|
|
82
|
-
status = 'complete' if batches_pending == batches_failed && jobs_pending == jobs_failed
|
|
83
|
-
status = 'success' if batches_pending == 0 && jobs_pending == 0
|
|
84
|
-
status = 'deleted' if bid != root_bid && !batch.parent_bid
|
|
85
|
-
|
|
86
|
-
{
|
|
87
|
-
bid: bid,
|
|
88
|
-
created_at: r.hget("BID-#{bid}", 'created_at'),
|
|
89
|
-
status: status,
|
|
90
|
-
parent_bid: parent ? parent.bid : batch.parent_bid,
|
|
91
|
-
description: batch.description,
|
|
92
|
-
jobs: {
|
|
93
|
-
pending_count: jobs_pending,
|
|
94
|
-
successful_count: jobs_success,
|
|
95
|
-
failed_count: jobs_failed,
|
|
96
|
-
dead_count: jobs_dead,
|
|
97
|
-
total_count: jobs_total,
|
|
98
|
-
# items: batches.map{|b| layer_data[b] },
|
|
99
|
-
},
|
|
100
|
-
batches: {
|
|
101
|
-
pending_count: batches_pending,
|
|
102
|
-
successful_count: batches_success,
|
|
103
|
-
failed_count: batches_failed,
|
|
104
|
-
total_count: batches_total,
|
|
105
|
-
items: layer[1].map{|b| layer_data[b, batch] },
|
|
106
|
-
},
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
data = layer_data[tree_bids]
|
|
111
|
-
data[:batches][:slice] = slice if slice
|
|
112
|
-
data
|
|
113
|
-
end
|
|
114
|
-
end
|
|
115
|
-
|
|
116
|
-
def format_context(batch)
|
|
117
|
-
bits = []
|
|
118
|
-
own_keys = batch.context.own.keys
|
|
119
|
-
batch.context.flatten.each do |k,v|
|
|
120
|
-
added = own_keys.include? k
|
|
121
|
-
bits << " <span class=\"key #{added ? 'own' : 'inherited'}\">\"#{k}\": #{v.to_json},</span>"
|
|
122
|
-
end
|
|
123
|
-
bits = [
|
|
124
|
-
"{ // <span class=\"own\">Added</span> / <span class=\"inherited\">Inherited</span>",
|
|
125
|
-
*bits,
|
|
126
|
-
'}'
|
|
127
|
-
]
|
|
128
|
-
bits.join("\n")
|
|
129
|
-
end
|
|
148
|
+
json(tree_data(@bid, slice: url_params('slice')))
|
|
130
149
|
end
|
|
131
150
|
|
|
132
151
|
app.post "/batches/all" do
|
|
133
|
-
if
|
|
152
|
+
if url_params('delete')
|
|
134
153
|
index_key = Joblin::Batching::Batch::INDEX_ALL_BATCHES ? "batches" : "BID-ROOT-bids"
|
|
135
154
|
drain_zset(index_key) do |batches|
|
|
136
155
|
batches.each do |bid|
|
|
@@ -143,10 +162,10 @@ module Joblin::Batching::Compat::Sidekiq
|
|
|
143
162
|
end
|
|
144
163
|
|
|
145
164
|
app.post "/batches/:bid" do
|
|
146
|
-
@bid =
|
|
165
|
+
@bid = route_param(:bid)
|
|
147
166
|
@batch = Joblin::Batching::Batch.new(@bid)
|
|
148
167
|
|
|
149
|
-
if
|
|
168
|
+
if url_params('delete')
|
|
150
169
|
Joblin::Batching::Batch.delete_prematurely!(@bid)
|
|
151
170
|
end
|
|
152
171
|
|
|
@@ -156,28 +175,28 @@ module Joblin::Batching::Compat::Sidekiq
|
|
|
156
175
|
# =============== POOLS =============== #
|
|
157
176
|
|
|
158
177
|
app.get "/pools" do
|
|
159
|
-
@count = (
|
|
160
|
-
@current_page, @total_size, @pools = page('pools',
|
|
178
|
+
@count = (url_params('count') || 25).to_i
|
|
179
|
+
@current_page, @total_size, @pools = page('pools', url_params('page'), @count)
|
|
161
180
|
@pools = @pools.map {|b, score| Joblin::Batching::Pool.new(b) }
|
|
162
181
|
|
|
163
182
|
erb(get_template(:pools))
|
|
164
183
|
end
|
|
165
184
|
|
|
166
185
|
app.get "/pools/:pid" do
|
|
167
|
-
@pid =
|
|
186
|
+
@pid = route_param(:pid)
|
|
168
187
|
@pool = Joblin::Batching::Pool.new(@pid)
|
|
169
188
|
|
|
170
189
|
@active_tasks = @pool.active_jobs
|
|
171
190
|
|
|
172
|
-
@count = (
|
|
173
|
-
@current_jobs_page, @total_jobs_size, @jobs = page("POOLID-#{@pool.pid}-jobs",
|
|
191
|
+
@count = (url_params('count') || 25).to_i
|
|
192
|
+
@current_jobs_page, @total_jobs_size, @jobs = page("POOLID-#{@pool.pid}-jobs", url_params('job_page'), @count)
|
|
174
193
|
@jobs = @jobs.map {|desc, score=nil| JSON.parse(desc)[0] }
|
|
175
194
|
|
|
176
195
|
erb(get_template(:pool))
|
|
177
196
|
end
|
|
178
197
|
|
|
179
198
|
app.post "/pools/all" do
|
|
180
|
-
if
|
|
199
|
+
if url_params('delete')
|
|
181
200
|
drain_zset('pools') do |pools|
|
|
182
201
|
pools.each do |pid|
|
|
183
202
|
Joblin::Batching::Pool.from_pid(pid).cleanup_redis
|
|
@@ -189,16 +208,56 @@ module Joblin::Batching::Compat::Sidekiq
|
|
|
189
208
|
end
|
|
190
209
|
|
|
191
210
|
app.post "/pools/:pid" do
|
|
192
|
-
@pid =
|
|
211
|
+
@pid = route_param(:pid)
|
|
193
212
|
@pool = Joblin::Batching::Pool.from_pid(@pid)
|
|
194
213
|
|
|
195
|
-
if
|
|
214
|
+
if url_params('delete')
|
|
196
215
|
@pool.cleanup_redis
|
|
197
216
|
end
|
|
198
217
|
|
|
199
218
|
redirect_with_query("#{root_path}pools")
|
|
200
219
|
end
|
|
201
220
|
end
|
|
221
|
+
|
|
222
|
+
# Register this extension's Batches + Pools tabs against `web_class`,
|
|
223
|
+
# choosing the call shape that matches the installed Sidekiq.
|
|
224
|
+
#
|
|
225
|
+
# The `Sidekiq::Web.register` API changed twice across the supported range,
|
|
226
|
+
# so we capability-detect rather than parse version strings:
|
|
227
|
+
#
|
|
228
|
+
# * Sidekiq 7.3.9+ and 8.x expose `Sidekiq::Web.configure`. The Sidekiq 8
|
|
229
|
+
# `register` emits a deprecation warning unless called through the
|
|
230
|
+
# `configure` block, so we use that. The keyword `register` registers the
|
|
231
|
+
# tabs itself via the tab/index zip, so we do not also set `tabs[...]`.
|
|
232
|
+
# * Sidekiq 7.3.0-7.3.8 have the keyword `register` but no `configure`. We
|
|
233
|
+
# call `register(ext, name:, tab:, index:)` directly; it too registers the
|
|
234
|
+
# tabs from the zip.
|
|
235
|
+
# * Sidekiq <= 7.2.x only have the single-argument `register(extension)`,
|
|
236
|
+
# which just calls `extension.registered(app)` and never touches `tabs`.
|
|
237
|
+
# There we fall back to the original behavior: register, then set the tabs
|
|
238
|
+
# manually.
|
|
239
|
+
#
|
|
240
|
+
# Detect the keyword form by inspecting `register`'s parameters for the
|
|
241
|
+
# `name:` keyword instead of relying on a Sidekiq version constant.
|
|
242
|
+
def self.register_tabs(web_class) # rubocop:disable Metrics/MethodLength
|
|
243
|
+
register_args = {
|
|
244
|
+
name: "joblin_batches",
|
|
245
|
+
tab: ["Batches", "Pools"],
|
|
246
|
+
index: ["batches", "pools"]
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if web_class.respond_to?(:configure)
|
|
250
|
+
web_class.configure do |cfg|
|
|
251
|
+
cfg.register(self, **register_args)
|
|
252
|
+
end
|
|
253
|
+
elsif web_class.method(:register).parameters.any? { |type, name| name == :name && (type == :key || type == :keyreq) }
|
|
254
|
+
web_class.register(self, **register_args)
|
|
255
|
+
else
|
|
256
|
+
web_class.register(self)
|
|
257
|
+
web_class.tabs["Batches"] = "batches"
|
|
258
|
+
web_class.tabs["Pools"] = "pools"
|
|
259
|
+
end
|
|
260
|
+
end
|
|
202
261
|
end
|
|
203
262
|
end
|
|
204
263
|
|
|
@@ -206,13 +265,23 @@ if defined?(::Sidekiq::Web)
|
|
|
206
265
|
rules = []
|
|
207
266
|
rules = [[:all, {"Cache-Control" => "public, max-age=86400"}]] unless Joblin::Batching::Compat::Sidekiq::Web::DEV_MODE
|
|
208
267
|
|
|
268
|
+
# Serve the bundled JS/CSS at /batches_assets. The erb views reference these
|
|
269
|
+
# URLs directly, so we keep this manual mount rather than Sidekiq 8's
|
|
270
|
+
# name-namespaced asset convention (which would change the URLs).
|
|
209
271
|
::Sidekiq::Web.use Rack::Static, urls: ["/batches_assets"],
|
|
210
272
|
root: File.expand_path("#{File.dirname(__FILE__)}/web"),
|
|
211
273
|
cascade: true,
|
|
212
274
|
header_rules: rules
|
|
213
275
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
276
|
+
Joblin::Batching::Compat::Sidekiq::Web.register_tabs(::Sidekiq::Web)
|
|
277
|
+
|
|
278
|
+
# The original code appended a `locales` directory that does not exist in this
|
|
279
|
+
# gem; preserve that harmless behavior without crashing if `settings` is gone
|
|
280
|
+
# (Sidekiq 8 removed `Sidekiq::Web.settings`; `locales` is available on both).
|
|
281
|
+
locales_dir = File.join(File.dirname(__FILE__), "locales")
|
|
282
|
+
if ::Sidekiq::Web.respond_to?(:locales)
|
|
283
|
+
::Sidekiq::Web.locales << locales_dir
|
|
284
|
+
elsif ::Sidekiq::Web.respond_to?(:settings)
|
|
285
|
+
::Sidekiq::Web.settings.locales << locales_dir
|
|
286
|
+
end
|
|
218
287
|
end
|
|
@@ -81,10 +81,21 @@ module Joblin::Batching
|
|
|
81
81
|
end
|
|
82
82
|
end
|
|
83
83
|
|
|
84
|
+
# Sidekiq 8 renamed the ActiveJob wrapper class. Accept both so
|
|
85
|
+
# ActiveJob-based jobs stay batch-aware across Sidekiq 7 and 8.
|
|
86
|
+
ACTIVE_JOB_WRAPPERS = [
|
|
87
|
+
'ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper',
|
|
88
|
+
'Sidekiq::ActiveJob::Wrapper',
|
|
89
|
+
].freeze
|
|
90
|
+
|
|
84
91
|
def self.is_activejob_job?(msg)
|
|
85
92
|
return false unless defined?(::ActiveJob)
|
|
93
|
+
return false unless ACTIVE_JOB_WRAPPERS.include?(msg['class'])
|
|
86
94
|
|
|
87
|
-
|
|
95
|
+
# `wrapped` comes from the job payload; use safe_constantize so a
|
|
96
|
+
# malformed/unknown value yields nil rather than raising NameError.
|
|
97
|
+
wrapped = msg['wrapped'].to_s.safe_constantize
|
|
98
|
+
!!(wrapped && wrapped < Joblin::Batching::Compat::ActiveJob::BatchAwareJob)
|
|
88
99
|
end
|
|
89
100
|
|
|
90
101
|
def self.switch_tenant(job)
|
data/lib/joblin/batching/pool.rb
CHANGED
|
File without changes
|
|
@@ -73,10 +73,21 @@ module Joblin::Uniqueness
|
|
|
73
73
|
end
|
|
74
74
|
end
|
|
75
75
|
|
|
76
|
+
# Sidekiq 8 renamed the ActiveJob wrapper class. Accept both so
|
|
77
|
+
# ActiveJob-based jobs keep their unique-job handling across Sidekiq 7 and 8.
|
|
78
|
+
ACTIVE_JOB_WRAPPERS = [
|
|
79
|
+
'ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper',
|
|
80
|
+
'Sidekiq::ActiveJob::Wrapper',
|
|
81
|
+
].freeze
|
|
82
|
+
|
|
76
83
|
def self.is_activejob_job?(msg)
|
|
77
84
|
return false unless defined?(::ActiveJob)
|
|
85
|
+
return false unless ACTIVE_JOB_WRAPPERS.include?(msg['class'])
|
|
78
86
|
|
|
79
|
-
|
|
87
|
+
# `wrapped` comes from the job payload; use safe_constantize so a
|
|
88
|
+
# malformed/unknown value yields nil rather than raising NameError.
|
|
89
|
+
wrapped = msg['wrapped'].to_s.safe_constantize
|
|
90
|
+
!!(wrapped && wrapped < Compat::ActiveJob::UniqueJobExtension)
|
|
80
91
|
end
|
|
81
92
|
|
|
82
93
|
def self.validate_middleware_order(chain, order)
|
data/lib/joblin/version.rb
CHANGED
|
@@ -111,6 +111,36 @@ RSpec.describe Joblin::Batching::Compat::Sidekiq do
|
|
|
111
111
|
end
|
|
112
112
|
end
|
|
113
113
|
|
|
114
|
+
describe '.is_activejob_job?' do
|
|
115
|
+
let(:wrapped_class) do
|
|
116
|
+
Class.new(ActiveJob::Base) do
|
|
117
|
+
include Joblin::Batching::Compat::ActiveJob::BatchAwareJob
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
before { stub_const('WrappedBatchAwareJob', wrapped_class) }
|
|
122
|
+
|
|
123
|
+
it 'returns true for the legacy Sidekiq 7 wrapper class' do
|
|
124
|
+
msg = { 'class' => 'ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper', 'wrapped' => 'WrappedBatchAwareJob' }
|
|
125
|
+
expect(Joblin::Batching::Compat::Sidekiq.is_activejob_job?(msg)).to be_truthy
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
it 'returns true for the Sidekiq 8 wrapper class' do
|
|
129
|
+
msg = { 'class' => 'Sidekiq::ActiveJob::Wrapper', 'wrapped' => 'WrappedBatchAwareJob' }
|
|
130
|
+
expect(Joblin::Batching::Compat::Sidekiq.is_activejob_job?(msg)).to be_truthy
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
it 'returns false for a plain (non-ActiveJob) job' do
|
|
134
|
+
msg = { 'class' => 'SomePlainWorker' }
|
|
135
|
+
expect(Joblin::Batching::Compat::Sidekiq.is_activejob_job?(msg)).to be_falsey
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
it 'returns false without raising for a wrapper whose wrapped class is unknown' do
|
|
139
|
+
msg = { 'class' => 'Sidekiq::ActiveJob::Wrapper', 'wrapped' => 'No::Such::Constant' }
|
|
140
|
+
expect(Joblin::Batching::Compat::Sidekiq.is_activejob_job?(msg)).to be_falsey
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
114
144
|
context 'worker' do
|
|
115
145
|
it 'defines method bid' do
|
|
116
146
|
expect(Sidekiq::Worker.instance_methods).to include(:bid)
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
# Sidekiq's Web UI isn't loadable in every supported environment: Sidekiq 6
|
|
4
|
+
# pulls in `rack/file`, which Rack 3 removed, so `require "sidekiq/web"` raises
|
|
5
|
+
# LoadError there. joblin's own web compat guards the same require, so skip
|
|
6
|
+
# these specs rather than fail when Web can't load. A non-LoadError (e.g. the
|
|
7
|
+
# registration regression this file exists to catch) still propagates.
|
|
8
|
+
begin
|
|
9
|
+
require 'sidekiq/web'
|
|
10
|
+
require 'joblin/batching/compat/sidekiq/web'
|
|
11
|
+
rescue LoadError
|
|
12
|
+
return
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Regression coverage for https://github.com/.../joblin web compat.
|
|
16
|
+
#
|
|
17
|
+
# joblin's web extension used to call `Sidekiq::Web.register(extension)` with a
|
|
18
|
+
# single positional argument and `Sidekiq::Web.settings.locales << ...`. Both of
|
|
19
|
+
# those crash on Sidekiq 8: `register` now requires the `name:`/`tab:`/`index:`
|
|
20
|
+
# keywords, and `Sidekiq::Web.settings` was removed. These specs assert that
|
|
21
|
+
# loading the compat file registers the Batches and Pools tabs without raising
|
|
22
|
+
# on whichever Sidekiq major is installed, so the bug cannot silently return.
|
|
23
|
+
RSpec.describe Joblin::Batching::Compat::Sidekiq::Web do
|
|
24
|
+
describe 'tab registration' do
|
|
25
|
+
it 'registers a Batches tab pointing at the batches index' do
|
|
26
|
+
expect(::Sidekiq::Web.tabs['Batches']).to eq('batches')
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
it 'registers a Pools tab pointing at the pools index' do
|
|
30
|
+
expect(::Sidekiq::Web.tabs['Pools']).to eq('pools')
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
describe '.register_tabs (capability branch selection)' do
|
|
35
|
+
# The bundled Sidekiq is 7.3.9, so only the `configure` branch runs against
|
|
36
|
+
# the real gem. These doubles exercise all three branches regardless of the
|
|
37
|
+
# installed major, so an older-Sidekiq regression cannot slip through.
|
|
38
|
+
#
|
|
39
|
+
# The single-arg `register(extension)` double calls `registered` on the real
|
|
40
|
+
# extension, so we register against the same fake-app collaborator the
|
|
41
|
+
# `.registered` examples use to keep the route bodies from blowing up.
|
|
42
|
+
let(:fake_app) do
|
|
43
|
+
Class.new do
|
|
44
|
+
attr_reader :routes, :helper_modules
|
|
45
|
+
|
|
46
|
+
def initialize
|
|
47
|
+
@routes = []
|
|
48
|
+
@helper_modules = []
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def helpers(mod = nil, &block)
|
|
52
|
+
@helper_modules << (mod || block)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def get(path, &block)
|
|
56
|
+
@routes << [:get, path, block]
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def post(path, &block)
|
|
60
|
+
@routes << [:post, path, block]
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
it 'uses configure when the web class is configure-capable (Sidekiq 7.3.9+/8.x)' do
|
|
66
|
+
tabs = {}
|
|
67
|
+
registered_with = nil
|
|
68
|
+
cfg = Object.new
|
|
69
|
+
cfg.define_singleton_method(:register) do |ext, name:, tab:, index:|
|
|
70
|
+
registered_with = { ext: ext, name: name }
|
|
71
|
+
tab.zip(index).each { |t, i| tabs[t] = i }
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
web_class = Object.new
|
|
75
|
+
web_class.define_singleton_method(:configure) { |&blk| blk.call(cfg) }
|
|
76
|
+
# configure-capable classes also respond to :register, but the branch must
|
|
77
|
+
# prefer configure to avoid the Sidekiq 8 deprecation warning.
|
|
78
|
+
web_class.define_singleton_method(:register) { |*, **| raise 'should not call register directly' }
|
|
79
|
+
|
|
80
|
+
expect { described_class.register_tabs(web_class) }.not_to raise_error
|
|
81
|
+
expect(registered_with[:ext]).to eq(described_class)
|
|
82
|
+
expect(tabs).to eq('Batches' => 'batches', 'Pools' => 'pools')
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
it 'calls keyword register directly when configure is absent (Sidekiq 7.3.0-7.3.8)' do
|
|
86
|
+
tabs = {}
|
|
87
|
+
web_class = Object.new
|
|
88
|
+
web_class.define_singleton_method(:register) do |ext, name: nil, tab: nil, index: nil|
|
|
89
|
+
Array(tab).zip(Array(index)).each { |t, i| tabs[t] = i }
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
expect { described_class.register_tabs(web_class) }.not_to raise_error
|
|
93
|
+
expect(tabs).to eq('Batches' => 'batches', 'Pools' => 'pools')
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
it 'falls back to single-arg register plus manual tabs (Sidekiq <= 7.2.x)' do
|
|
97
|
+
app = fake_app.new
|
|
98
|
+
tabs = {}
|
|
99
|
+
web_class = Object.new
|
|
100
|
+
web_class.define_singleton_method(:tabs) { tabs }
|
|
101
|
+
# Single-arg register mirrors real Sidekiq 7.2: it just calls back into the
|
|
102
|
+
# extension's `registered` and never touches tabs.
|
|
103
|
+
web_class.define_singleton_method(:register) do |ext|
|
|
104
|
+
ext.registered(app)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
expect { described_class.register_tabs(web_class) }.not_to raise_error
|
|
108
|
+
expect(tabs).to eq('Batches' => 'batches', 'Pools' => 'pools')
|
|
109
|
+
paths = app.routes.map { |(_method, path, _block)| path }
|
|
110
|
+
expect(paths).to include('/batches', '/pools')
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
describe '.registered' do
|
|
115
|
+
# `register` ran once at load time above; re-running against a fresh fake
|
|
116
|
+
# application proves the registration path itself does not raise on the
|
|
117
|
+
# installed Sidekiq major. This is the path that crashed on Sidekiq 8.
|
|
118
|
+
let(:fake_app) do
|
|
119
|
+
Class.new do
|
|
120
|
+
# Capture the routes/helpers the extension tries to install so the call
|
|
121
|
+
# exercises the real `registered` body without needing a live Rack app.
|
|
122
|
+
attr_reader :routes, :helper_modules
|
|
123
|
+
|
|
124
|
+
def initialize
|
|
125
|
+
@routes = []
|
|
126
|
+
@helper_modules = []
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def helpers(mod = nil, &block)
|
|
130
|
+
@helper_modules << (mod || block)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def get(path, &block)
|
|
134
|
+
@routes << [:get, path, block]
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def post(path, &block)
|
|
138
|
+
@routes << [:post, path, block]
|
|
139
|
+
end
|
|
140
|
+
end.new
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
it 'installs the batches and pools routes without raising' do
|
|
144
|
+
expect { described_class.registered(fake_app) }.not_to raise_error
|
|
145
|
+
|
|
146
|
+
paths = fake_app.routes.map { |(_method, path, _block)| path }
|
|
147
|
+
expect(paths).to include('/batches', '/batches/:bid', '/pools', '/pools/:pid')
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
it 'installs helpers as a module (Sidekiq 8 only accepts modules)' do
|
|
151
|
+
described_class.registered(fake_app)
|
|
152
|
+
|
|
153
|
+
expect(fake_app.helper_modules).to all(be_a(Module))
|
|
154
|
+
expect(fake_app.helper_modules).to include(Joblin::Batching::Compat::Sidekiq::Web::RouteHelpers)
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
describe Joblin::Batching::Compat::Sidekiq::Web::RouteHelpers do
|
|
159
|
+
# `route_param` is the cross-version shim for path params such as `:bid`.
|
|
160
|
+
# On Sidekiq 7 `params[:bid]` worked; on Sidekiq 8 it does not, so we read
|
|
161
|
+
# the Rack env directly. Verify it pulls the value regardless of key type.
|
|
162
|
+
let(:helper_host) do
|
|
163
|
+
Class.new do
|
|
164
|
+
include Joblin::Batching::Compat::Sidekiq::Web::RouteHelpers
|
|
165
|
+
|
|
166
|
+
attr_accessor :env
|
|
167
|
+
|
|
168
|
+
def initialize(env)
|
|
169
|
+
@env = env
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
it 'reads route params from the Rack env' do
|
|
175
|
+
host = helper_host.new('rack.route_params' => { bid: 'ABC123' })
|
|
176
|
+
expect(host.route_param(:bid)).to eq('ABC123')
|
|
177
|
+
expect(host.route_param('bid')).to eq('ABC123')
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
it 'returns nil when no route params are present' do
|
|
181
|
+
host = helper_host.new({})
|
|
182
|
+
expect(host.route_param(:bid)).to be_nil
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
end
|