sidekiq 6.5.12 → 7.3.9

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.
Files changed (123) hide show
  1. checksums.yaml +4 -4
  2. data/Changes.md +340 -20
  3. data/README.md +43 -35
  4. data/bin/multi_queue_bench +271 -0
  5. data/bin/sidekiq +3 -8
  6. data/bin/sidekiqload +213 -118
  7. data/bin/sidekiqmon +3 -0
  8. data/lib/active_job/queue_adapters/sidekiq_adapter.rb +75 -0
  9. data/lib/generators/sidekiq/job_generator.rb +2 -0
  10. data/lib/sidekiq/api.rb +243 -162
  11. data/lib/sidekiq/capsule.rb +132 -0
  12. data/lib/sidekiq/cli.rb +60 -75
  13. data/lib/sidekiq/client.rb +87 -38
  14. data/lib/sidekiq/component.rb +26 -1
  15. data/lib/sidekiq/config.rb +311 -0
  16. data/lib/sidekiq/deploy.rb +64 -0
  17. data/lib/sidekiq/embedded.rb +63 -0
  18. data/lib/sidekiq/fetch.rb +11 -14
  19. data/lib/sidekiq/iterable_job.rb +55 -0
  20. data/lib/sidekiq/job/interrupt_handler.rb +24 -0
  21. data/lib/sidekiq/job/iterable/active_record_enumerator.rb +53 -0
  22. data/lib/sidekiq/job/iterable/csv_enumerator.rb +47 -0
  23. data/lib/sidekiq/job/iterable/enumerators.rb +135 -0
  24. data/lib/sidekiq/job/iterable.rb +294 -0
  25. data/lib/sidekiq/job.rb +382 -10
  26. data/lib/sidekiq/job_logger.rb +8 -7
  27. data/lib/sidekiq/job_retry.rb +42 -19
  28. data/lib/sidekiq/job_util.rb +53 -15
  29. data/lib/sidekiq/launcher.rb +71 -65
  30. data/lib/sidekiq/logger.rb +2 -27
  31. data/lib/sidekiq/manager.rb +9 -11
  32. data/lib/sidekiq/metrics/query.rb +9 -4
  33. data/lib/sidekiq/metrics/shared.rb +21 -9
  34. data/lib/sidekiq/metrics/tracking.rb +40 -26
  35. data/lib/sidekiq/middleware/chain.rb +19 -18
  36. data/lib/sidekiq/middleware/current_attributes.rb +85 -20
  37. data/lib/sidekiq/middleware/modules.rb +2 -0
  38. data/lib/sidekiq/monitor.rb +18 -4
  39. data/lib/sidekiq/paginator.rb +8 -2
  40. data/lib/sidekiq/processor.rb +62 -57
  41. data/lib/sidekiq/rails.rb +27 -10
  42. data/lib/sidekiq/redis_client_adapter.rb +31 -71
  43. data/lib/sidekiq/redis_connection.rb +44 -115
  44. data/lib/sidekiq/ring_buffer.rb +2 -0
  45. data/lib/sidekiq/scheduled.rb +22 -23
  46. data/lib/sidekiq/systemd.rb +2 -0
  47. data/lib/sidekiq/testing.rb +37 -46
  48. data/lib/sidekiq/transaction_aware_client.rb +11 -5
  49. data/lib/sidekiq/version.rb +6 -1
  50. data/lib/sidekiq/web/action.rb +29 -7
  51. data/lib/sidekiq/web/application.rb +82 -28
  52. data/lib/sidekiq/web/csrf_protection.rb +10 -7
  53. data/lib/sidekiq/web/helpers.rb +110 -49
  54. data/lib/sidekiq/web/router.rb +5 -2
  55. data/lib/sidekiq/web.rb +70 -17
  56. data/lib/sidekiq/worker_compatibility_alias.rb +13 -0
  57. data/lib/sidekiq.rb +78 -274
  58. data/sidekiq.gemspec +13 -10
  59. data/web/assets/javascripts/application.js +44 -0
  60. data/web/assets/javascripts/base-charts.js +106 -0
  61. data/web/assets/javascripts/dashboard-charts.js +194 -0
  62. data/web/assets/javascripts/dashboard.js +17 -233
  63. data/web/assets/javascripts/metrics.js +151 -115
  64. data/web/assets/stylesheets/application-dark.css +4 -0
  65. data/web/assets/stylesheets/application-rtl.css +10 -89
  66. data/web/assets/stylesheets/application.css +56 -296
  67. data/web/locales/ar.yml +70 -70
  68. data/web/locales/cs.yml +62 -62
  69. data/web/locales/da.yml +60 -53
  70. data/web/locales/de.yml +65 -65
  71. data/web/locales/el.yml +2 -7
  72. data/web/locales/en.yml +81 -71
  73. data/web/locales/es.yml +68 -68
  74. data/web/locales/fa.yml +65 -65
  75. data/web/locales/fr.yml +80 -67
  76. data/web/locales/gd.yml +98 -0
  77. data/web/locales/he.yml +65 -64
  78. data/web/locales/hi.yml +59 -59
  79. data/web/locales/it.yml +85 -54
  80. data/web/locales/ja.yml +67 -70
  81. data/web/locales/ko.yml +52 -52
  82. data/web/locales/lt.yml +66 -66
  83. data/web/locales/nb.yml +61 -61
  84. data/web/locales/nl.yml +52 -52
  85. data/web/locales/pl.yml +45 -45
  86. data/web/locales/pt-br.yml +78 -69
  87. data/web/locales/pt.yml +51 -51
  88. data/web/locales/ru.yml +67 -66
  89. data/web/locales/sv.yml +53 -53
  90. data/web/locales/ta.yml +60 -60
  91. data/web/locales/tr.yml +100 -0
  92. data/web/locales/uk.yml +85 -61
  93. data/web/locales/ur.yml +64 -64
  94. data/web/locales/vi.yml +67 -67
  95. data/web/locales/zh-cn.yml +20 -19
  96. data/web/locales/zh-tw.yml +10 -2
  97. data/web/views/_footer.erb +16 -2
  98. data/web/views/_job_info.erb +18 -2
  99. data/web/views/_metrics_period_select.erb +12 -0
  100. data/web/views/_paging.erb +2 -0
  101. data/web/views/_poll_link.erb +1 -1
  102. data/web/views/_summary.erb +7 -7
  103. data/web/views/busy.erb +46 -35
  104. data/web/views/dashboard.erb +32 -8
  105. data/web/views/filtering.erb +6 -0
  106. data/web/views/layout.erb +6 -6
  107. data/web/views/metrics.erb +47 -26
  108. data/web/views/metrics_for_job.erb +43 -71
  109. data/web/views/morgue.erb +7 -11
  110. data/web/views/queue.erb +11 -15
  111. data/web/views/queues.erb +9 -3
  112. data/web/views/retries.erb +5 -9
  113. data/web/views/scheduled.erb +12 -13
  114. metadata +66 -41
  115. data/lib/sidekiq/delay.rb +0 -43
  116. data/lib/sidekiq/extensions/action_mailer.rb +0 -48
  117. data/lib/sidekiq/extensions/active_record.rb +0 -43
  118. data/lib/sidekiq/extensions/class_methods.rb +0 -43
  119. data/lib/sidekiq/extensions/generic_proxy.rb +0 -33
  120. data/lib/sidekiq/metrics/deploy.rb +0 -47
  121. data/lib/sidekiq/worker.rb +0 -370
  122. data/web/assets/javascripts/graph.js +0 -16
  123. /data/{LICENSE → LICENSE.txt} +0 -0
@@ -5,11 +5,18 @@ require "sidekiq/client"
5
5
 
6
6
  module Sidekiq
7
7
  class TransactionAwareClient
8
- def initialize(redis_pool)
9
- @redis_client = Client.new(redis_pool)
8
+ def initialize(pool: nil, config: nil)
9
+ @redis_client = Client.new(pool: pool, config: config)
10
+ end
11
+
12
+ def batching?
13
+ Thread.current[:sidekiq_batch]
10
14
  end
11
15
 
12
16
  def push(item)
17
+ # 6160 we can't support both Sidekiq::Batch and transactions.
18
+ return @redis_client.push(item) if batching?
19
+
13
20
  # pre-allocate the JID so we can return it immediately and
14
21
  # save it to the database as part of the transaction.
15
22
  item["jid"] ||= SecureRandom.hex(12)
@@ -34,11 +41,10 @@ module Sidekiq
34
41
  begin
35
42
  require "after_commit_everywhere"
36
43
  rescue LoadError
37
- Sidekiq.logger.error("You need to add after_commit_everywhere to your Gemfile to use Sidekiq's transactional client")
38
- raise
44
+ raise %q(You need to add `gem "after_commit_everywhere"` to your Gemfile to use Sidekiq's transactional client)
39
45
  end
40
46
 
41
- default_job_options["client_class"] = Sidekiq::TransactionAwareClient
47
+ Sidekiq.default_job_options["client_class"] = Sidekiq::TransactionAwareClient
42
48
  Sidekiq::JobUtil::TRANSIENT_ATTRIBUTES << "client_class"
43
49
  true
44
50
  end
@@ -1,5 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Sidekiq
4
- VERSION = "6.5.12"
4
+ VERSION = "7.3.9"
5
+ MAJOR = 7
6
+
7
+ def self.gem_version
8
+ Gem::Version.new(VERSION)
9
+ end
5
10
  end
@@ -15,13 +15,19 @@ module Sidekiq
15
15
  end
16
16
 
17
17
  def halt(res)
18
- throw :halt, [res, {"content-type" => "text/plain"}, [res.to_s]]
18
+ throw :halt, [res, {Rack::CONTENT_TYPE => "text/plain"}, [res.to_s]]
19
19
  end
20
20
 
21
21
  def redirect(location)
22
- throw :halt, [302, {"location" => "#{request.base_url}#{location}"}, []]
22
+ throw :halt, [302, {Web::LOCATION => "#{request.base_url}#{location}"}, []]
23
23
  end
24
24
 
25
+ def reload_page
26
+ current_location = request.referer.gsub(request.base_url, "")
27
+ redirect current_location
28
+ end
29
+
30
+ # deprecated, will warn in 8.0
25
31
  def params
26
32
  indifferent_hash = Hash.new { |hash, key| hash[key.to_s] if Symbol === key }
27
33
 
@@ -31,8 +37,19 @@ module Sidekiq
31
37
  indifferent_hash
32
38
  end
33
39
 
34
- def route_params
35
- env[WebRouter::ROUTE_PARAMS]
40
+ # Use like `url_params("page")` within your action blocks
41
+ def url_params(key)
42
+ request.params[key]
43
+ end
44
+
45
+ # Use like `route_params(:name)` within your action blocks
46
+ # key is required in 8.0, nil is only used for backwards compatibility
47
+ def route_params(key = nil)
48
+ if key
49
+ env[WebRouter::ROUTE_PARAMS][key]
50
+ else
51
+ env[WebRouter::ROUTE_PARAMS]
52
+ end
36
53
  end
37
54
 
38
55
  def session
@@ -42,8 +59,13 @@ module Sidekiq
42
59
  def erb(content, options = {})
43
60
  if content.is_a? Symbol
44
61
  unless respond_to?(:"_erb_#{content}")
45
- src = ERB.new(File.read("#{Web.settings.views}/#{content}.erb")).src
46
- WebAction.class_eval <<-RUBY, __FILE__, __LINE__ + 1
62
+ views = options[:views] || Web.settings.views
63
+ filename = "#{views}/#{content}.erb"
64
+ src = ERB.new(File.read(filename)).src
65
+
66
+ # Need to use lineno less by 1 because erb generates a
67
+ # comment before the source code.
68
+ WebAction.class_eval <<-RUBY, filename, -1 # standard:disable Style/EvalWithLocation
47
69
  def _erb_#{content}
48
70
  #{src}
49
71
  end
@@ -68,7 +90,7 @@ module Sidekiq
68
90
  end
69
91
 
70
92
  def json(payload)
71
- [200, {"content-type" => "application/json", "cache-control" => "private, no-store"}, [Sidekiq.dump_json(payload)]]
93
+ [200, {Rack::CONTENT_TYPE => "application/json", Rack::CACHE_CONTROL => "private, no-store"}, [Sidekiq.dump_json(payload)]]
72
94
  end
73
95
 
74
96
  def initialize(env, block)
@@ -5,7 +5,7 @@ module Sidekiq
5
5
  extend WebRouter
6
6
 
7
7
  REDIS_KEYS = %w[redis_version uptime_in_days connected_clients used_memory_human used_memory_peak_human]
8
- CSP_HEADER = [
8
+ CSP_HEADER_TEMPLATE = [
9
9
  "default-src 'self' https: http:",
10
10
  "child-src 'self'",
11
11
  "connect-src 'self' https: http: wss: ws:",
@@ -15,11 +15,17 @@ module Sidekiq
15
15
  "manifest-src 'self'",
16
16
  "media-src 'self'",
17
17
  "object-src 'none'",
18
- "script-src 'self' https: http: 'unsafe-inline'",
19
- "style-src 'self' https: http: 'unsafe-inline'",
18
+ "script-src 'self' 'nonce-!placeholder!'",
19
+ "style-src 'self' https: http: 'unsafe-inline'", # TODO Nonce in 8.0
20
20
  "worker-src 'self'",
21
21
  "base-uri 'self'"
22
22
  ].join("; ").freeze
23
+ METRICS_PERIODS = {
24
+ "1h" => 60,
25
+ "2h" => 120,
26
+ "4h" => 240,
27
+ "8h" => 480
28
+ }
23
29
 
24
30
  def initialize(klass)
25
31
  @klass = klass
@@ -43,9 +49,9 @@ module Sidekiq
43
49
 
44
50
  head "/" do
45
51
  # HEAD / is the cheapest heartbeat possible,
46
- # it hits Redis to ensure connectivity
47
- Sidekiq.redis { |c| c.llen("queue:default") }
48
- ""
52
+ # it hits Redis to ensure connectivity and returns
53
+ # the size of the default queue
54
+ Sidekiq.redis { |c| c.llen("queue:default") }.to_s
49
55
  end
50
56
 
51
57
  get "/" do
@@ -61,15 +67,25 @@ module Sidekiq
61
67
  end
62
68
 
63
69
  get "/metrics" do
70
+ x = params[:substr]
71
+ class_filter = (x.nil? || x == "") ? nil : Regexp.new(Regexp.escape(x), Regexp::IGNORECASE)
72
+
64
73
  q = Sidekiq::Metrics::Query.new
65
- @query_result = q.top_jobs
74
+ @period = h((params[:period] || "")[0..1])
75
+ @periods = METRICS_PERIODS
76
+ minutes = @periods.fetch(@period, @periods.values.first)
77
+ @query_result = q.top_jobs(minutes: minutes, class_filter: class_filter)
78
+
66
79
  erb(:metrics)
67
80
  end
68
81
 
69
82
  get "/metrics/:name" do
70
83
  @name = route_params[:name]
84
+ @period = h((params[:period] || "")[0..1])
71
85
  q = Sidekiq::Metrics::Query.new
72
- @query_result = q.for_job(@name)
86
+ @periods = METRICS_PERIODS
87
+ minutes = @periods.fetch(@period, @periods.values.first)
88
+ @query_result = q.for_job(@name, minutes: minutes)
73
89
  erb(:metrics_for_job)
74
90
  end
75
91
 
@@ -82,11 +98,14 @@ module Sidekiq
82
98
 
83
99
  post "/busy" do
84
100
  if params["identity"]
85
- p = Sidekiq::Process.new("identity" => params["identity"])
86
- p.quiet! if params["quiet"]
87
- p.stop! if params["stop"]
101
+ pro = Sidekiq::ProcessSet[params["identity"]]
102
+
103
+ pro.quiet! if params["quiet"]
104
+ pro.stop! if params["stop"]
88
105
  else
89
106
  processes.each do |pro|
107
+ next if pro.embedded?
108
+
90
109
  pro.quiet! if params["quiet"]
91
110
  pro.stop! if params["stop"]
92
111
  end
@@ -138,9 +157,15 @@ module Sidekiq
138
157
  end
139
158
 
140
159
  get "/morgue" do
141
- @count = (params["count"] || 25).to_i
142
- (@current_page, @total_size, @dead) = page("dead", params["page"], @count, reverse: true)
143
- @dead = @dead.map { |msg, score| Sidekiq::SortedEntry.new(nil, score, msg) }
160
+ x = params[:substr]
161
+
162
+ if x && x != ""
163
+ @dead = search(Sidekiq::DeadSet.new, x)
164
+ else
165
+ @count = (params["count"] || 25).to_i
166
+ (@current_page, @total_size, @dead) = page("dead", params["page"], @count, reverse: true)
167
+ @dead = @dead.map { |msg, score| Sidekiq::SortedEntry.new(nil, score, msg) }
168
+ end
144
169
 
145
170
  erb(:morgue)
146
171
  end
@@ -159,7 +184,7 @@ module Sidekiq
159
184
  end
160
185
 
161
186
  post "/morgue" do
162
- redirect(request.path) unless params["key"]
187
+ redirect(request.path) unless url_params("key")
163
188
 
164
189
  params["key"].each do |key|
165
190
  job = Sidekiq::DeadSet.new.fetch(*parse_params(key)).first
@@ -182,7 +207,7 @@ module Sidekiq
182
207
  end
183
208
 
184
209
  post "/morgue/:key" do
185
- key = route_params[:key]
210
+ key = route_params(:key)
186
211
  halt(404) unless key
187
212
 
188
213
  job = Sidekiq::DeadSet.new.fetch(*parse_params(key)).first
@@ -192,9 +217,15 @@ module Sidekiq
192
217
  end
193
218
 
194
219
  get "/retries" do
195
- @count = (params["count"] || 25).to_i
196
- (@current_page, @total_size, @retries) = page("retry", params["page"], @count)
197
- @retries = @retries.map { |msg, score| Sidekiq::SortedEntry.new(nil, score, msg) }
220
+ x = url_params("substr")
221
+
222
+ if x && x != ""
223
+ @retries = search(Sidekiq::RetrySet.new, x)
224
+ else
225
+ @count = (params["count"] || 25).to_i
226
+ (@current_page, @total_size, @retries) = page("retry", params["page"], @count)
227
+ @retries = @retries.map { |msg, score| Sidekiq::SortedEntry.new(nil, score, msg) }
228
+ end
198
229
 
199
230
  erb(:retries)
200
231
  end
@@ -247,9 +278,15 @@ module Sidekiq
247
278
  end
248
279
 
249
280
  get "/scheduled" do
250
- @count = (params["count"] || 25).to_i
251
- (@current_page, @total_size, @scheduled) = page("schedule", params["page"], @count)
252
- @scheduled = @scheduled.map { |msg, score| Sidekiq::SortedEntry.new(nil, score, msg) }
281
+ x = params[:substr]
282
+
283
+ if x && x != ""
284
+ @scheduled = search(Sidekiq::ScheduledSet.new, x)
285
+ else
286
+ @count = (params["count"] || 25).to_i
287
+ (@current_page, @total_size, @scheduled) = page("schedule", params["page"], @count)
288
+ @scheduled = @scheduled.map { |msg, score| Sidekiq::SortedEntry.new(nil, score, msg) }
289
+ end
253
290
 
254
291
  erb(:scheduled)
255
292
  end
@@ -310,12 +347,24 @@ module Sidekiq
310
347
  end
311
348
 
312
349
  get "/stats/queues" do
313
- json Sidekiq::Stats::Queues.new.lengths
350
+ json Sidekiq::Stats.new.queues
351
+ end
352
+
353
+ post "/change_locale" do
354
+ locale = params["locale"]
355
+
356
+ match = available_locales.find { |available|
357
+ locale == available
358
+ }
359
+
360
+ session[:locale] = match if match
361
+
362
+ reload_page
314
363
  end
315
364
 
316
365
  def call(env)
317
366
  action = self.class.match(env)
318
- return [404, {"content-type" => "text/plain", "x-cascade" => "pass"}, ["Not Found"]] unless action
367
+ return [404, {Rack::CONTENT_TYPE => "text/plain", Web::X_CASCADE => "pass"}, ["Not Found"]] unless action
319
368
 
320
369
  app = @klass
321
370
  resp = catch(:halt) do
@@ -332,16 +381,21 @@ module Sidekiq
332
381
  else
333
382
  # rendered content goes here
334
383
  headers = {
335
- "content-type" => "text/html",
336
- "cache-control" => "private, no-store",
337
- "content-language" => action.locale,
338
- "content-security-policy" => CSP_HEADER
384
+ Rack::CONTENT_TYPE => "text/html",
385
+ Rack::CACHE_CONTROL => "private, no-store",
386
+ Web::CONTENT_LANGUAGE => action.locale,
387
+ Web::CONTENT_SECURITY_POLICY => process_csp(env, CSP_HEADER_TEMPLATE),
388
+ Web::X_CONTENT_TYPE_OPTIONS => "nosniff"
339
389
  }
340
390
  # we'll let Rack calculate Content-Length for us.
341
391
  [200, headers, [resp]]
342
392
  end
343
393
  end
344
394
 
395
+ def process_csp(env, input)
396
+ input.gsub("!placeholder!", env[:csp_nonce])
397
+ end
398
+
345
399
  def self.helpers(mod = nil, &block)
346
400
  if block
347
401
  WebAction.class_eval(&block)
@@ -27,7 +27,6 @@
27
27
  # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
28
28
 
29
29
  require "securerandom"
30
- require "base64"
31
30
  require "rack/request"
32
31
 
33
32
  module Sidekiq
@@ -57,12 +56,12 @@ module Sidekiq
57
56
  end
58
57
 
59
58
  def logger(env)
60
- @logger ||= (env["rack.logger"] || ::Logger.new(env["rack.errors"]))
59
+ @logger ||= env["rack.logger"] || ::Logger.new(env["rack.errors"])
61
60
  end
62
61
 
63
62
  def deny(env)
64
63
  logger(env).warn "attack prevented by #{self.class}"
65
- [403, {"Content-Type" => "text/plain"}, ["Forbidden"]]
64
+ [403, {Rack::CONTENT_TYPE => "text/plain"}, ["Forbidden"]]
66
65
  end
67
66
 
68
67
  def session(env)
@@ -116,7 +115,7 @@ module Sidekiq
116
115
  sess = session(env)
117
116
  localtoken = sess[:csrf]
118
117
 
119
- # Checks that Rack::Session::Cookie actualy contains the csrf toekn
118
+ # Checks that Rack::Session::Cookie actually contains the csrf token
120
119
  return false if localtoken.nil?
121
120
 
122
121
  # Rotate the session token after every use
@@ -143,7 +142,7 @@ module Sidekiq
143
142
  one_time_pad = SecureRandom.random_bytes(token.length)
144
143
  encrypted_token = xor_byte_strings(one_time_pad, token)
145
144
  masked_token = one_time_pad + encrypted_token
146
- Base64.urlsafe_encode64(masked_token)
145
+ encode_token(masked_token)
147
146
  end
148
147
 
149
148
  # Essentially the inverse of +mask_token+.
@@ -152,7 +151,7 @@ module Sidekiq
152
151
  # value and decrypt it
153
152
  token_length = masked_token.length / 2
154
153
  one_time_pad = masked_token[0...token_length]
155
- encrypted_token = masked_token[token_length..-1]
154
+ encrypted_token = masked_token[token_length..]
156
155
  xor_byte_strings(one_time_pad, encrypted_token)
157
156
  end
158
157
 
@@ -168,8 +167,12 @@ module Sidekiq
168
167
  ::Rack::Utils.secure_compare(token.to_s, decode_token(local).to_s)
169
168
  end
170
169
 
170
+ def encode_token(token)
171
+ [token].pack("m0").tr("+/", "-_")
172
+ end
173
+
171
174
  def decode_token(token)
172
- Base64.urlsafe_decode64(token)
175
+ token.tr("-_", "+/").unpack1("m0")
173
176
  end
174
177
 
175
178
  def xor_byte_strings(s1, s2)
@@ -6,21 +6,68 @@ require "yaml"
6
6
  require "cgi"
7
7
 
8
8
  module Sidekiq
9
- # This is not a public API
9
+ # These methods are available to pages within the Web UI and UI extensions.
10
+ # They are not public APIs for applications to use.
10
11
  module WebHelpers
12
+ def style_tag(location, **kwargs)
13
+ global = location.match?(/:\/\//)
14
+ location = root_path + location if !global && !location.start_with?(root_path)
15
+ attrs = {
16
+ type: "text/css",
17
+ media: "screen",
18
+ rel: "stylesheet",
19
+ nonce: csp_nonce,
20
+ href: location
21
+ }
22
+ html_tag(:link, attrs.merge(kwargs))
23
+ end
24
+
25
+ def script_tag(location, **kwargs)
26
+ global = location.match?(/:\/\//)
27
+ location = root_path + location if !global && !location.start_with?(root_path)
28
+ attrs = {
29
+ type: "text/javascript",
30
+ nonce: csp_nonce,
31
+ src: location
32
+ }
33
+ html_tag(:script, attrs.merge(kwargs)) {}
34
+ end
35
+
36
+ # NB: keys and values are not escaped; do not allow user input
37
+ # in the attributes
38
+ private def html_tag(tagname, attrs)
39
+ s = +"<#{tagname}"
40
+ attrs.each_pair do |k, v|
41
+ next unless v
42
+ s << " #{k}=\"#{v}\""
43
+ end
44
+ if block_given?
45
+ s << ">"
46
+ yield s
47
+ s << "</#{tagname}>"
48
+ else
49
+ s << " />"
50
+ end
51
+ s
52
+ end
53
+
11
54
  def strings(lang)
12
- @strings ||= {}
55
+ @@strings ||= {}
13
56
 
14
57
  # Allow sidekiq-web extensions to add locale paths
15
58
  # so extensions can be localized
16
- @strings[lang] ||= settings.locales.each_with_object({}) do |path, global|
59
+ @@strings[lang] ||= settings.locales.each_with_object({}) do |path, global|
17
60
  find_locale_files(lang).each do |file|
18
- strs = YAML.safe_load(File.open(file))
61
+ strs = YAML.safe_load(File.read(file))
19
62
  global.merge!(strs[lang])
20
63
  end
21
64
  end
22
65
  end
23
66
 
67
+ def to_json(x)
68
+ Sidekiq.dump_json(x)
69
+ end
70
+
24
71
  def singularize(str, count)
25
72
  if count == 1 && str.respond_to?(:singularize) # rails
26
73
  str.singularize
@@ -30,27 +77,48 @@ module Sidekiq
30
77
  end
31
78
 
32
79
  def clear_caches
33
- @strings = nil
34
- @locale_files = nil
35
- @available_locales = nil
80
+ @@strings = nil
81
+ @@locale_files = nil
82
+ @@available_locales = nil
36
83
  end
37
84
 
38
85
  def locale_files
39
- @locale_files ||= settings.locales.flat_map { |path|
86
+ @@locale_files ||= settings.locales.flat_map { |path|
40
87
  Dir["#{path}/*.yml"]
41
88
  }
42
89
  end
43
90
 
44
91
  def available_locales
45
- @available_locales ||= locale_files.map { |path| File.basename(path, ".yml") }.uniq
92
+ @@available_locales ||= Set.new(locale_files.map { |path| File.basename(path, ".yml") })
46
93
  end
47
94
 
48
95
  def find_locale_files(lang)
49
96
  locale_files.select { |file| file =~ /\/#{lang}\.yml$/ }
50
97
  end
51
98
 
52
- # This is a hook for a Sidekiq Pro feature. Please don't touch.
53
- def filtering(*)
99
+ def search(jobset, substr)
100
+ resultset = jobset.scan(substr).to_a
101
+ @current_page = 1
102
+ @count = @total_size = resultset.size
103
+ resultset
104
+ end
105
+
106
+ def filtering(which)
107
+ erb(:filtering, locals: {which: which})
108
+ end
109
+
110
+ def filter_link(jid, within = "retries")
111
+ if within.nil?
112
+ ::Rack::Utils.escape_html(jid)
113
+ else
114
+ "<a href='#{root_path}#{within}?substr=#{jid}'>#{::Rack::Utils.escape_html(jid)}</a>"
115
+ end
116
+ end
117
+
118
+ def display_tags(job, within = "retries")
119
+ job.tags.map { |tag|
120
+ "<span class='label label-info jobtag'>#{filter_link(tag, within)}</span>"
121
+ }.join(" ")
54
122
  end
55
123
 
56
124
  # This view helper provide ability display you html code in
@@ -96,7 +164,10 @@ module Sidekiq
96
164
  #
97
165
  # Inspiration taken from https://github.com/iain/http_accept_language/blob/master/lib/http_accept_language/parser.rb
98
166
  def locale
99
- @locale ||= begin
167
+ # session[:locale] is set via the locale selector from the footer
168
+ @locale ||= if (l = session&.fetch(:locale, nil)) && available_locales.include?(l)
169
+ l
170
+ else
100
171
  matched_locale = user_preferred_languages.map { |preferred|
101
172
  preferred_language = preferred.split("-", 2).first
102
173
 
@@ -111,16 +182,10 @@ module Sidekiq
111
182
  end
112
183
  end
113
184
 
114
- # within is used by Sidekiq Pro
115
- def display_tags(job, within = nil)
116
- job.tags.map { |tag|
117
- "<span class='label label-info jobtag'>#{::Rack::Utils.escape_html(tag)}</span>"
118
- }.join(" ")
119
- end
120
-
121
- # mperham/sidekiq#3243
185
+ # sidekiq/sidekiq#3243
122
186
  def unfiltered?
123
- yield unless env["PATH_INFO"].start_with?("/filter/")
187
+ s = url_params("substr")
188
+ yield unless s && s.size > 0
124
189
  end
125
190
 
126
191
  def get_locale
@@ -161,22 +226,26 @@ module Sidekiq
161
226
  end
162
227
  end
163
228
 
229
+ def busy_weights(capsule_weights)
230
+ # backwards compat with 7.0.0, remove in 7.1
231
+ cw = [capsule_weights].flatten
232
+ cw.map { |hash|
233
+ hash.map { |name, weight| (weight > 0) ? +name << ": " << weight.to_s : name }.join(", ")
234
+ }.join("; ")
235
+ end
236
+
164
237
  def stats
165
238
  @stats ||= Sidekiq::Stats.new
166
239
  end
167
240
 
168
- def redis_connection
241
+ def redis_url
169
242
  Sidekiq.redis do |conn|
170
- conn.connection[:id]
243
+ conn.config.server_url
171
244
  end
172
245
  end
173
246
 
174
- def namespace
175
- @ns ||= Sidekiq.redis { |conn| conn.respond_to?(:namespace) ? conn.namespace : nil }
176
- end
177
-
178
247
  def redis_info
179
- Sidekiq.redis_info
248
+ Sidekiq.default_configuration.redis_info
180
249
  end
181
250
 
182
251
  def root_path
@@ -241,6 +310,10 @@ module Sidekiq
241
310
  "<input type='hidden' name='authenticity_token' value='#{env[:csrf_token]}'/>"
242
311
  end
243
312
 
313
+ def csp_nonce
314
+ env[:csp_nonce]
315
+ end
316
+
244
317
  def to_display(arg)
245
318
  arg.inspect
246
319
  rescue
@@ -274,27 +347,17 @@ module Sidekiq
274
347
  elsif rss_kb < 10_000_000
275
348
  "#{number_with_delimiter((rss_kb / 1024.0).to_i)} MB"
276
349
  else
277
- "#{number_with_delimiter((rss_kb / (1024.0 * 1024.0)).round(1))} GB"
350
+ "#{number_with_delimiter((rss_kb / (1024.0 * 1024.0)), precision: 1)} GB"
278
351
  end
279
352
  end
280
353
 
281
- def number_with_delimiter(number)
282
- return "" if number.nil?
283
-
284
- begin
285
- Float(number)
286
- rescue ArgumentError, TypeError
287
- return number
288
- end
289
-
290
- options = {delimiter: ",", separator: "."}
291
- parts = number.to_s.to_str.split(".")
292
- parts[0].gsub!(/(\d)(?=(\d\d\d)+(?!\d))/, "\\1#{options[:delimiter]}")
293
- parts.join(options[:separator])
354
+ def number_with_delimiter(number, options = {})
355
+ precision = options[:precision] || 0
356
+ %(<span data-nwp="#{precision}">#{number.round(precision)}</span>)
294
357
  end
295
358
 
296
359
  def h(text)
297
- ::Rack::Utils.escape_html(text)
360
+ ::Rack::Utils.escape_html(text.to_s)
298
361
  rescue ArgumentError => e
299
362
  raise unless e.message.eql?("invalid byte sequence in UTF-8")
300
363
  text.encode!("UTF-16", "UTF-8", invalid: :replace, replace: "").encode!("UTF-8", "UTF-16")
@@ -314,7 +377,7 @@ module Sidekiq
314
377
  end
315
378
 
316
379
  def environment_title_prefix
317
- environment = Sidekiq[:environment] || ENV["APP_ENV"] || ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "development"
380
+ environment = Sidekiq.default_configuration[:environment] || ENV["APP_ENV"] || ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "development"
318
381
 
319
382
  "[#{environment.upcase}] " unless environment == "production"
320
383
  end
@@ -327,11 +390,9 @@ module Sidekiq
327
390
  Time.now.utc.strftime("%H:%M:%S UTC")
328
391
  end
329
392
 
330
- def redis_connection_and_namespace
331
- @redis_connection_and_namespace ||= begin
332
- namespace_suffix = namespace.nil? ? "" : "##{namespace}"
333
- "#{redis_connection}#{namespace_suffix}"
334
- end
393
+ def pollable?
394
+ # there's no point to refreshing the metrics pages every N seconds
395
+ !(current_path == "" || current_path.index("metrics"))
335
396
  end
336
397
 
337
398
  def retry_or_delete_or_kill(job, params)
@@ -39,10 +39,13 @@ module Sidekiq
39
39
  route(DELETE, path, &block)
40
40
  end
41
41
 
42
- def route(method, path, &block)
42
+ def route(*methods, path, &block)
43
43
  @routes ||= {GET => [], POST => [], PUT => [], PATCH => [], DELETE => [], HEAD => []}
44
44
 
45
- @routes[method] << WebRoute.new(method, path, block)
45
+ methods.each do |method|
46
+ method = method.to_s.upcase
47
+ @routes[method] << WebRoute.new(method, path, block)
48
+ end
46
49
  end
47
50
 
48
51
  def match(env)