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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0b97d9567bb5853da62d6dc2dd5fb8be9c4542b8edce5ac3c8c04a41911cba4c
4
- data.tar.gz: fe8836c6aa2f974d1647f4d5c80b3d4f6c7913a1ac5750fa06ba6d8901b825e7
3
+ metadata.gz: fee4e742fd313bae02c2d077066985c147749f207dfd211ac5adb29426a80d84
4
+ data.tar.gz: d0d074e5a51d65ee15a63e23a549240014f2d09c882ce5632163ab4a9a444c7a
5
5
  SHA512:
6
- metadata.gz: a5a5b430bf022fd87de5bc8cf045f14b8621dcc6f198ba5cbd2c08144e7d15e664a18e4afb3e09160014b0ab0cd70e799681744c4c9937295ea96890cc5e4367
7
- data.tar.gz: 06d5967211f519e0ab1875b63424e946147d3bcd395c499516d784e306f2fedd32ad9d99ca58a1bfcf3343c920473dfd86f6a69c6a5dc297e66486f4d019f2ce
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
- def self.registered(app) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
17
- app.helpers do
18
- include Web::Helpers
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
- def dev_mode?
21
- DEV_MODE
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 = (params['count'] || 25).to_i
118
+ @count = (url_params('count') || 25).to_i
29
119
 
30
- source_key = params['all_batches'] ? "batches" : "BID-ROOT-bids"
31
- @current_page, @total_size, @batches = page(source_key, params['page'], @count)
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 = params[: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 = (params['count'] || 25).to_i
44
- @current_batches_page, @total_batches_size, @sub_batches = page("BID-#{@batch.bid}-bids", params['batch_page'], @count)
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", params['job_page'], @count)
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 = params[:bid]
146
+ @bid = route_param(:bid)
57
147
 
58
- json(tree_data(@bid, slice: params[: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 params['delete']
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 = params[:bid]
165
+ @bid = route_param(:bid)
147
166
  @batch = Joblin::Batching::Batch.new(@bid)
148
167
 
149
- if params['delete']
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 = (params['count'] || 25).to_i
160
- @current_page, @total_size, @pools = page('pools', params['page'], @count)
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 = params[: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 = (params['count'] || 25).to_i
173
- @current_jobs_page, @total_jobs_size, @jobs = page("POOLID-#{@pool.pid}-jobs", params['job_page'], @count)
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 params['delete']
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 = params[:pid]
211
+ @pid = route_param(:pid)
193
212
  @pool = Joblin::Batching::Pool.from_pid(@pid)
194
213
 
195
- if params['delete']
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
- ::Sidekiq::Web.register Joblin::Batching::Compat::Sidekiq::Web
215
- ::Sidekiq::Web.tabs["Batches"] = "batches"
216
- ::Sidekiq::Web.tabs["Pools"] = "pools"
217
- ::Sidekiq::Web.settings.locales << File.join(File.dirname(__FILE__), "locales")
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
- msg['class'] == 'ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper' && (msg['wrapped'].to_s).constantize < Joblin::Batching::Compat::ActiveJob::BatchAwareJob
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)
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
- msg['class'] == 'ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper' && (msg['wrapped'].to_s).constantize < Compat::ActiveJob::UniqueJobExtension
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)
@@ -1,3 +1,3 @@
1
1
  module Joblin
2
- VERSION = "0.1.10".freeze
2
+ VERSION = "0.1.11".freeze
3
3
  end
@@ -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