joblin 0.1.10 → 0.1.12

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: 988601efd5b12dd1866c4dd0e6adbe223fd33421929e3bf78ad0ee799a3e6e53
4
+ data.tar.gz: 850bc134faba3424e2bbbce58c7d3daefa4d37d8d76f55c92a7602242f46565a
5
5
  SHA512:
6
- metadata.gz: a5a5b430bf022fd87de5bc8cf045f14b8621dcc6f198ba5cbd2c08144e7d15e664a18e4afb3e09160014b0ab0cd70e799681744c4c9937295ea96890cc5e4367
7
- data.tar.gz: 06d5967211f519e0ab1875b63424e946147d3bcd395c499516d784e306f2fedd32ad9d99ca58a1bfcf3343c920473dfd86f6a69c6a5dc297e66486f4d019f2ce
6
+ metadata.gz: 727eac8e2f058579bf1f109b0f3faa5658e166c895ec37be6a12c9052c8982a8b6ba46debbc3fff8b780cd4af5a92d28c87164bf26baa3850df2297917db2085
7
+ data.tar.gz: a9c4b246ca613eeb98fc464f3e333a6ff9135fe14c51e4b8fe949d87fe2789da4cc4909d86f77362696b343b252d3bac246a88902df18dd86fc933dcdac16a51
@@ -10,41 +10,143 @@ 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
41
+
42
+ # Read a query-string parameter such as `count` or `page`.
43
+ #
44
+ # Sidekiq 7+ defines `url_params(key)` on the web action and returns the
45
+ # query value only. Sidekiq 6.x has no `url_params`; there the action's
46
+ # `params` is an indifferent hash that already merges the query string,
47
+ # so we read from it. On Sidekiq 7/8 the action's own `url_params`
48
+ # shadows this fallback (a class's instance method wins over one from an
49
+ # included module), so this definition only takes effect on Sidekiq 6.x.
50
+ def url_params(key)
51
+ params[key]
52
+ end
53
+
54
+ def tree_data(root_bid, slice: nil)
55
+ tree_bids = Joblin::Batching::Batch.bid_hierarchy(root_bid, slice: slice)
56
+
57
+ Joblin::Batching::Batch.redis do |r|
58
+ layer_data = ->(layer, parent = nil) {
59
+ bid = layer[0]
60
+ batch = Joblin::Batching::Batch.new(bid)
61
+
62
+ jobs_total = r.hget("BID-#{bid}", "job_count").to_i
63
+ jobs_pending = r.hget("BID-#{bid}", 'pending').to_i
64
+ jobs_failed = r.scard("BID-#{bid}-failed").to_i
65
+ jobs_dead = r.scard("BID-#{bid}-dead").to_i
66
+ jobs_success = jobs_total - jobs_pending
67
+
68
+ batches_total = r.hget("BID-#{bid}", 'children').to_i
69
+ batches_success = r.scard("BID-#{bid}-batches-success").to_i
70
+ batches_pending = batches_total - batches_success
71
+ batches_failed = r.scard("BID-#{bid}-batches-failed").to_i
72
+
73
+ status = 'in_progress'
74
+ status = 'complete' if batches_pending == batches_failed && jobs_pending == jobs_failed
75
+ status = 'success' if batches_pending == 0 && jobs_pending == 0
76
+ status = 'deleted' if bid != root_bid && !batch.parent_bid
77
+
78
+ {
79
+ bid: bid,
80
+ created_at: r.hget("BID-#{bid}", 'created_at'),
81
+ status: status,
82
+ parent_bid: parent ? parent.bid : batch.parent_bid,
83
+ description: batch.description,
84
+ jobs: {
85
+ pending_count: jobs_pending,
86
+ successful_count: jobs_success,
87
+ failed_count: jobs_failed,
88
+ dead_count: jobs_dead,
89
+ total_count: jobs_total,
90
+ # items: batches.map{|b| layer_data[b] },
91
+ },
92
+ batches: {
93
+ pending_count: batches_pending,
94
+ successful_count: batches_success,
95
+ failed_count: batches_failed,
96
+ total_count: batches_total,
97
+ items: layer[1].map{|b| layer_data[b, batch] },
98
+ },
99
+ }
100
+ }
101
+
102
+ data = layer_data[tree_bids]
103
+ data[:batches][:slice] = slice if slice
104
+ data
105
+ end
106
+ end
19
107
 
20
- def dev_mode?
21
- DEV_MODE
108
+ def format_context(batch)
109
+ bits = []
110
+ own_keys = batch.context.own.keys
111
+ batch.context.flatten.each do |k,v|
112
+ added = own_keys.include? k
113
+ bits << " <span class=\"key #{added ? 'own' : 'inherited'}\">\"#{k}\": #{v.to_json},</span>"
22
114
  end
115
+ bits = [
116
+ "{ // <span class=\"own\">Added</span> / <span class=\"inherited\">Inherited</span>",
117
+ *bits,
118
+ '}'
119
+ ]
120
+ bits.join("\n")
23
121
  end
122
+ end
123
+
124
+ def self.registered(app) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
125
+ app.helpers(RouteHelpers)
24
126
 
25
127
  # =============== BATCHES =============== #
26
128
 
27
129
  app.get "/batches" do
28
- @count = (params['count'] || 25).to_i
130
+ @count = (url_params('count') || 25).to_i
29
131
 
30
- source_key = params['all_batches'] ? "batches" : "BID-ROOT-bids"
31
- @current_page, @total_size, @batches = page(source_key, params['page'], @count)
132
+ source_key = url_params('all_batches') ? "batches" : "BID-ROOT-bids"
133
+ @current_page, @total_size, @batches = page(source_key, url_params('page'), @count)
32
134
  @batches = @batches.map {|b, score| Joblin::Batching::Batch.new(b) }
33
135
 
34
136
  erb(get_template(:batches))
35
137
  end
36
138
 
37
139
  app.get "/batches/:bid" do
38
- @bid = params[:bid]
140
+ @bid = route_param(:bid)
39
141
  @batch = Joblin::Batching::Batch.new(@bid)
40
142
 
41
143
  @tree_data = tree_data(@bid)
42
144
 
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)
145
+ @count = (url_params('count') || 25).to_i
146
+ @current_batches_page, @total_batches_size, @sub_batches = page("BID-#{@batch.bid}-bids", url_params('batch_page'), @count)
45
147
  @sub_batches = @sub_batches.map {|b, score| Joblin::Batching::Batch.new(b) }
46
148
 
47
- @current_jobs_page, @total_jobs_size, @jobs = page("BID-#{@batch.bid}-jids", params['job_page'], @count)
149
+ @current_jobs_page, @total_jobs_size, @jobs = page("BID-#{@batch.bid}-jids", url_params('job_page'), @count)
48
150
  @jobs = @jobs.map do |jid, score|
49
151
  { jid: jid, }
50
152
  end
@@ -53,84 +155,13 @@ module Joblin::Batching::Compat::Sidekiq
53
155
  end
54
156
 
55
157
  app.get "/batches/:bid/tree" do
56
- @bid = params[:bid]
57
-
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
158
+ @bid = route_param(:bid)
115
159
 
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
160
+ json(tree_data(@bid, slice: url_params('slice')))
130
161
  end
131
162
 
132
163
  app.post "/batches/all" do
133
- if params['delete']
164
+ if url_params('delete')
134
165
  index_key = Joblin::Batching::Batch::INDEX_ALL_BATCHES ? "batches" : "BID-ROOT-bids"
135
166
  drain_zset(index_key) do |batches|
136
167
  batches.each do |bid|
@@ -143,10 +174,10 @@ module Joblin::Batching::Compat::Sidekiq
143
174
  end
144
175
 
145
176
  app.post "/batches/:bid" do
146
- @bid = params[:bid]
177
+ @bid = route_param(:bid)
147
178
  @batch = Joblin::Batching::Batch.new(@bid)
148
179
 
149
- if params['delete']
180
+ if url_params('delete')
150
181
  Joblin::Batching::Batch.delete_prematurely!(@bid)
151
182
  end
152
183
 
@@ -156,28 +187,28 @@ module Joblin::Batching::Compat::Sidekiq
156
187
  # =============== POOLS =============== #
157
188
 
158
189
  app.get "/pools" do
159
- @count = (params['count'] || 25).to_i
160
- @current_page, @total_size, @pools = page('pools', params['page'], @count)
190
+ @count = (url_params('count') || 25).to_i
191
+ @current_page, @total_size, @pools = page('pools', url_params('page'), @count)
161
192
  @pools = @pools.map {|b, score| Joblin::Batching::Pool.new(b) }
162
193
 
163
194
  erb(get_template(:pools))
164
195
  end
165
196
 
166
197
  app.get "/pools/:pid" do
167
- @pid = params[:pid]
198
+ @pid = route_param(:pid)
168
199
  @pool = Joblin::Batching::Pool.new(@pid)
169
200
 
170
201
  @active_tasks = @pool.active_jobs
171
202
 
172
- @count = (params['count'] || 25).to_i
173
- @current_jobs_page, @total_jobs_size, @jobs = page("POOLID-#{@pool.pid}-jobs", params['job_page'], @count)
203
+ @count = (url_params('count') || 25).to_i
204
+ @current_jobs_page, @total_jobs_size, @jobs = page("POOLID-#{@pool.pid}-jobs", url_params('job_page'), @count)
174
205
  @jobs = @jobs.map {|desc, score=nil| JSON.parse(desc)[0] }
175
206
 
176
207
  erb(get_template(:pool))
177
208
  end
178
209
 
179
210
  app.post "/pools/all" do
180
- if params['delete']
211
+ if url_params('delete')
181
212
  drain_zset('pools') do |pools|
182
213
  pools.each do |pid|
183
214
  Joblin::Batching::Pool.from_pid(pid).cleanup_redis
@@ -189,16 +220,56 @@ module Joblin::Batching::Compat::Sidekiq
189
220
  end
190
221
 
191
222
  app.post "/pools/:pid" do
192
- @pid = params[:pid]
223
+ @pid = route_param(:pid)
193
224
  @pool = Joblin::Batching::Pool.from_pid(@pid)
194
225
 
195
- if params['delete']
226
+ if url_params('delete')
196
227
  @pool.cleanup_redis
197
228
  end
198
229
 
199
230
  redirect_with_query("#{root_path}pools")
200
231
  end
201
232
  end
233
+
234
+ # Register this extension's Batches + Pools tabs against `web_class`,
235
+ # choosing the call shape that matches the installed Sidekiq.
236
+ #
237
+ # The `Sidekiq::Web.register` API changed twice across the supported range,
238
+ # so we capability-detect rather than parse version strings:
239
+ #
240
+ # * Sidekiq 7.3.9+ and 8.x expose `Sidekiq::Web.configure`. The Sidekiq 8
241
+ # `register` emits a deprecation warning unless called through the
242
+ # `configure` block, so we use that. The keyword `register` registers the
243
+ # tabs itself via the tab/index zip, so we do not also set `tabs[...]`.
244
+ # * Sidekiq 7.3.0-7.3.8 have the keyword `register` but no `configure`. We
245
+ # call `register(ext, name:, tab:, index:)` directly; it too registers the
246
+ # tabs from the zip.
247
+ # * Sidekiq <= 7.2.x only have the single-argument `register(extension)`,
248
+ # which just calls `extension.registered(app)` and never touches `tabs`.
249
+ # There we fall back to the original behavior: register, then set the tabs
250
+ # manually.
251
+ #
252
+ # Detect the keyword form by inspecting `register`'s parameters for the
253
+ # `name:` keyword instead of relying on a Sidekiq version constant.
254
+ def self.register_tabs(web_class) # rubocop:disable Metrics/MethodLength
255
+ register_args = {
256
+ name: "joblin_batches",
257
+ tab: ["Batches", "Pools"],
258
+ index: ["batches", "pools"]
259
+ }
260
+
261
+ if web_class.respond_to?(:configure)
262
+ web_class.configure do |cfg|
263
+ cfg.register(self, **register_args)
264
+ end
265
+ elsif web_class.method(:register).parameters.any? { |type, name| name == :name && (type == :key || type == :keyreq) }
266
+ web_class.register(self, **register_args)
267
+ else
268
+ web_class.register(self)
269
+ web_class.tabs["Batches"] = "batches"
270
+ web_class.tabs["Pools"] = "pools"
271
+ end
272
+ end
202
273
  end
203
274
  end
204
275
 
@@ -206,13 +277,23 @@ if defined?(::Sidekiq::Web)
206
277
  rules = []
207
278
  rules = [[:all, {"Cache-Control" => "public, max-age=86400"}]] unless Joblin::Batching::Compat::Sidekiq::Web::DEV_MODE
208
279
 
280
+ # Serve the bundled JS/CSS at /batches_assets. The erb views reference these
281
+ # URLs directly, so we keep this manual mount rather than Sidekiq 8's
282
+ # name-namespaced asset convention (which would change the URLs).
209
283
  ::Sidekiq::Web.use Rack::Static, urls: ["/batches_assets"],
210
284
  root: File.expand_path("#{File.dirname(__FILE__)}/web"),
211
285
  cascade: true,
212
286
  header_rules: rules
213
287
 
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")
288
+ Joblin::Batching::Compat::Sidekiq::Web.register_tabs(::Sidekiq::Web)
289
+
290
+ # The original code appended a `locales` directory that does not exist in this
291
+ # gem; preserve that harmless behavior without crashing if `settings` is gone
292
+ # (Sidekiq 8 removed `Sidekiq::Web.settings`; `locales` is available on both).
293
+ locales_dir = File.join(File.dirname(__FILE__), "locales")
294
+ if ::Sidekiq::Web.respond_to?(:locales)
295
+ ::Sidekiq::Web.locales << locales_dir
296
+ elsif ::Sidekiq::Web.respond_to?(:settings)
297
+ ::Sidekiq::Web.settings.locales << locales_dir
298
+ end
218
299
  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.12".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