sidekiq 7.2.4 → 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 (71) hide show
  1. checksums.yaml +4 -4
  2. data/Changes.md +116 -0
  3. data/README.md +1 -1
  4. data/bin/sidekiqload +21 -12
  5. data/lib/active_job/queue_adapters/sidekiq_adapter.rb +75 -0
  6. data/lib/generators/sidekiq/job_generator.rb +2 -0
  7. data/lib/sidekiq/api.rb +63 -34
  8. data/lib/sidekiq/capsule.rb +8 -3
  9. data/lib/sidekiq/cli.rb +2 -1
  10. data/lib/sidekiq/client.rb +21 -1
  11. data/lib/sidekiq/component.rb +22 -0
  12. data/lib/sidekiq/config.rb +27 -3
  13. data/lib/sidekiq/deploy.rb +2 -0
  14. data/lib/sidekiq/embedded.rb +2 -0
  15. data/lib/sidekiq/fetch.rb +1 -1
  16. data/lib/sidekiq/iterable_job.rb +55 -0
  17. data/lib/sidekiq/job/interrupt_handler.rb +24 -0
  18. data/lib/sidekiq/job/iterable/active_record_enumerator.rb +53 -0
  19. data/lib/sidekiq/job/iterable/csv_enumerator.rb +47 -0
  20. data/lib/sidekiq/job/iterable/enumerators.rb +135 -0
  21. data/lib/sidekiq/job/iterable.rb +294 -0
  22. data/lib/sidekiq/job.rb +13 -2
  23. data/lib/sidekiq/job_logger.rb +7 -6
  24. data/lib/sidekiq/job_retry.rb +6 -1
  25. data/lib/sidekiq/job_util.rb +2 -0
  26. data/lib/sidekiq/launcher.rb +1 -1
  27. data/lib/sidekiq/metrics/query.rb +2 -0
  28. data/lib/sidekiq/metrics/shared.rb +15 -4
  29. data/lib/sidekiq/metrics/tracking.rb +13 -5
  30. data/lib/sidekiq/middleware/current_attributes.rb +46 -13
  31. data/lib/sidekiq/middleware/modules.rb +2 -0
  32. data/lib/sidekiq/monitor.rb +2 -1
  33. data/lib/sidekiq/paginator.rb +6 -0
  34. data/lib/sidekiq/processor.rb +20 -10
  35. data/lib/sidekiq/rails.rb +12 -0
  36. data/lib/sidekiq/redis_client_adapter.rb +8 -5
  37. data/lib/sidekiq/redis_connection.rb +33 -2
  38. data/lib/sidekiq/ring_buffer.rb +2 -0
  39. data/lib/sidekiq/systemd.rb +2 -0
  40. data/lib/sidekiq/testing.rb +5 -5
  41. data/lib/sidekiq/version.rb +5 -1
  42. data/lib/sidekiq/web/action.rb +21 -4
  43. data/lib/sidekiq/web/application.rb +43 -82
  44. data/lib/sidekiq/web/helpers.rb +62 -15
  45. data/lib/sidekiq/web/router.rb +5 -2
  46. data/lib/sidekiq/web.rb +54 -2
  47. data/lib/sidekiq.rb +5 -3
  48. data/sidekiq.gemspec +3 -2
  49. data/web/assets/javascripts/application.js +6 -1
  50. data/web/assets/javascripts/dashboard-charts.js +24 -12
  51. data/web/assets/javascripts/dashboard.js +7 -1
  52. data/web/assets/stylesheets/application.css +16 -3
  53. data/web/locales/en.yml +3 -1
  54. data/web/locales/fr.yml +0 -1
  55. data/web/locales/gd.yml +0 -1
  56. data/web/locales/it.yml +32 -1
  57. data/web/locales/ja.yml +0 -1
  58. data/web/locales/pt-br.yml +1 -2
  59. data/web/locales/tr.yml +100 -0
  60. data/web/locales/uk.yml +24 -1
  61. data/web/locales/zh-cn.yml +0 -1
  62. data/web/locales/zh-tw.yml +0 -1
  63. data/web/views/_footer.erb +1 -2
  64. data/web/views/dashboard.erb +10 -7
  65. data/web/views/filtering.erb +1 -2
  66. data/web/views/layout.erb +6 -6
  67. data/web/views/metrics.erb +7 -8
  68. data/web/views/metrics_for_job.erb +4 -4
  69. data/web/views/morgue.erb +2 -2
  70. data/web/views/queue.erb +1 -1
  71. metadata +32 -13
@@ -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,8 +15,8 @@ module Sidekiq
15
15
  "manifest-src 'self'",
16
16
  "media-src 'self'",
17
17
  "object-src 'none'",
18
- "script-src 'self' https: http:",
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
@@ -67,11 +67,15 @@ module Sidekiq
67
67
  end
68
68
 
69
69
  get "/metrics" do
70
+ x = params[:substr]
71
+ class_filter = (x.nil? || x == "") ? nil : Regexp.new(Regexp.escape(x), Regexp::IGNORECASE)
72
+
70
73
  q = Sidekiq::Metrics::Query.new
71
74
  @period = h((params[:period] || "")[0..1])
72
75
  @periods = METRICS_PERIODS
73
76
  minutes = @periods.fetch(@period, @periods.values.first)
74
- @query_result = q.top_jobs(minutes: minutes)
77
+ @query_result = q.top_jobs(minutes: minutes, class_filter: class_filter)
78
+
75
79
  erb(:metrics)
76
80
  end
77
81
 
@@ -153,9 +157,15 @@ module Sidekiq
153
157
  end
154
158
 
155
159
  get "/morgue" do
156
- @count = (params["count"] || 25).to_i
157
- (@current_page, @total_size, @dead) = page("dead", params["page"], @count, reverse: true)
158
- @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
159
169
 
160
170
  erb(:morgue)
161
171
  end
@@ -174,7 +184,7 @@ module Sidekiq
174
184
  end
175
185
 
176
186
  post "/morgue" do
177
- redirect(request.path) unless params["key"]
187
+ redirect(request.path) unless url_params("key")
178
188
 
179
189
  params["key"].each do |key|
180
190
  job = Sidekiq::DeadSet.new.fetch(*parse_params(key)).first
@@ -197,7 +207,7 @@ module Sidekiq
197
207
  end
198
208
 
199
209
  post "/morgue/:key" do
200
- key = route_params[:key]
210
+ key = route_params(:key)
201
211
  halt(404) unless key
202
212
 
203
213
  job = Sidekiq::DeadSet.new.fetch(*parse_params(key)).first
@@ -207,9 +217,15 @@ module Sidekiq
207
217
  end
208
218
 
209
219
  get "/retries" do
210
- @count = (params["count"] || 25).to_i
211
- (@current_page, @total_size, @retries) = page("retry", params["page"], @count)
212
- @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
213
229
 
214
230
  erb(:retries)
215
231
  end
@@ -262,9 +278,15 @@ module Sidekiq
262
278
  end
263
279
 
264
280
  get "/scheduled" do
265
- @count = (params["count"] || 25).to_i
266
- (@current_page, @total_size, @scheduled) = page("schedule", params["page"], @count)
267
- @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
268
290
 
269
291
  erb(:scheduled)
270
292
  end
@@ -328,72 +350,6 @@ module Sidekiq
328
350
  json Sidekiq::Stats.new.queues
329
351
  end
330
352
 
331
- ########
332
- # Filtering
333
-
334
- get "/filter/metrics" do
335
- redirect "#{root_path}metrics"
336
- end
337
-
338
- post "/filter/metrics" do
339
- x = params[:substr]
340
- q = Sidekiq::Metrics::Query.new
341
- @period = h((params[:period] || "")[0..1])
342
- @periods = METRICS_PERIODS
343
- minutes = @periods.fetch(@period, @periods.values.first)
344
- @query_result = q.top_jobs(minutes: minutes, class_filter: Regexp.new(Regexp.escape(x), Regexp::IGNORECASE))
345
-
346
- erb :metrics
347
- end
348
-
349
- get "/filter/retries" do
350
- x = params[:substr]
351
- return redirect "#{root_path}retries" unless x && x != ""
352
-
353
- @retries = search(Sidekiq::RetrySet.new, params[:substr])
354
- erb :retries
355
- end
356
-
357
- post "/filter/retries" do
358
- x = params[:substr]
359
- return redirect "#{root_path}retries" unless x && x != ""
360
-
361
- @retries = search(Sidekiq::RetrySet.new, params[:substr])
362
- erb :retries
363
- end
364
-
365
- get "/filter/scheduled" do
366
- x = params[:substr]
367
- return redirect "#{root_path}scheduled" unless x && x != ""
368
-
369
- @scheduled = search(Sidekiq::ScheduledSet.new, params[:substr])
370
- erb :scheduled
371
- end
372
-
373
- post "/filter/scheduled" do
374
- x = params[:substr]
375
- return redirect "#{root_path}scheduled" unless x && x != ""
376
-
377
- @scheduled = search(Sidekiq::ScheduledSet.new, params[:substr])
378
- erb :scheduled
379
- end
380
-
381
- get "/filter/dead" do
382
- x = params[:substr]
383
- return redirect "#{root_path}morgue" unless x && x != ""
384
-
385
- @dead = search(Sidekiq::DeadSet.new, params[:substr])
386
- erb :morgue
387
- end
388
-
389
- post "/filter/dead" do
390
- x = params[:substr]
391
- return redirect "#{root_path}morgue" unless x && x != ""
392
-
393
- @dead = search(Sidekiq::DeadSet.new, params[:substr])
394
- erb :morgue
395
- end
396
-
397
353
  post "/change_locale" do
398
354
  locale = params["locale"]
399
355
 
@@ -428,13 +384,18 @@ module Sidekiq
428
384
  Rack::CONTENT_TYPE => "text/html",
429
385
  Rack::CACHE_CONTROL => "private, no-store",
430
386
  Web::CONTENT_LANGUAGE => action.locale,
431
- Web::CONTENT_SECURITY_POLICY => CSP_HEADER
387
+ Web::CONTENT_SECURITY_POLICY => process_csp(env, CSP_HEADER_TEMPLATE),
388
+ Web::X_CONTENT_TYPE_OPTIONS => "nosniff"
432
389
  }
433
390
  # we'll let Rack calculate Content-Length for us.
434
391
  [200, headers, [resp]]
435
392
  end
436
393
  end
437
394
 
395
+ def process_csp(env, input)
396
+ input.gsub("!placeholder!", env[:csp_nonce])
397
+ end
398
+
438
399
  def self.helpers(mod = nil, &block)
439
400
  if block
440
401
  WebAction.class_eval(&block)
@@ -6,14 +6,57 @@ 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
61
  strs = YAML.safe_load(File.read(file))
19
62
  global.merge!(strs[lang])
@@ -34,19 +77,19 @@ module Sidekiq
34
77
  end
35
78
 
36
79
  def clear_caches
37
- @strings = nil
38
- @locale_files = nil
39
- @available_locales = nil
80
+ @@strings = nil
81
+ @@locale_files = nil
82
+ @@available_locales = nil
40
83
  end
41
84
 
42
85
  def locale_files
43
- @locale_files ||= settings.locales.flat_map { |path|
86
+ @@locale_files ||= settings.locales.flat_map { |path|
44
87
  Dir["#{path}/*.yml"]
45
88
  }
46
89
  end
47
90
 
48
91
  def available_locales
49
- @available_locales ||= locale_files.map { |path| File.basename(path, ".yml") }.uniq
92
+ @@available_locales ||= Set.new(locale_files.map { |path| File.basename(path, ".yml") })
50
93
  end
51
94
 
52
95
  def find_locale_files(lang)
@@ -68,7 +111,7 @@ module Sidekiq
68
111
  if within.nil?
69
112
  ::Rack::Utils.escape_html(jid)
70
113
  else
71
- "<a href='#{root_path}filter/#{within}?substr=#{jid}'>#{::Rack::Utils.escape_html(jid)}</a>"
114
+ "<a href='#{root_path}#{within}?substr=#{jid}'>#{::Rack::Utils.escape_html(jid)}</a>"
72
115
  end
73
116
  end
74
117
 
@@ -122,10 +165,9 @@ module Sidekiq
122
165
  # Inspiration taken from https://github.com/iain/http_accept_language/blob/master/lib/http_accept_language/parser.rb
123
166
  def locale
124
167
  # session[:locale] is set via the locale selector from the footer
125
- # defined?(session) && session are used to avoid exceptions when running tests
126
- return session[:locale] if defined?(session) && session&.[](:locale)
127
-
128
- @locale ||= begin
168
+ @locale ||= if (l = session&.fetch(:locale, nil)) && available_locales.include?(l)
169
+ l
170
+ else
129
171
  matched_locale = user_preferred_languages.map { |preferred|
130
172
  preferred_language = preferred.split("-", 2).first
131
173
 
@@ -142,7 +184,8 @@ module Sidekiq
142
184
 
143
185
  # sidekiq/sidekiq#3243
144
186
  def unfiltered?
145
- yield unless env["PATH_INFO"].start_with?("/filter/")
187
+ s = url_params("substr")
188
+ yield unless s && s.size > 0
146
189
  end
147
190
 
148
191
  def get_locale
@@ -267,6 +310,10 @@ module Sidekiq
267
310
  "<input type='hidden' name='authenticity_token' value='#{env[:csrf_token]}'/>"
268
311
  end
269
312
 
313
+ def csp_nonce
314
+ env[:csp_nonce]
315
+ end
316
+
270
317
  def to_display(arg)
271
318
  arg.inspect
272
319
  rescue
@@ -310,7 +357,7 @@ module Sidekiq
310
357
  end
311
358
 
312
359
  def h(text)
313
- ::Rack::Utils.escape_html(text)
360
+ ::Rack::Utils.escape_html(text.to_s)
314
361
  rescue ArgumentError => e
315
362
  raise unless e.message.eql?("invalid byte sequence in UTF-8")
316
363
  text.encode!("UTF-16", "UTF-8", invalid: :replace, replace: "").encode!("UTF-8", "UTF-16")
@@ -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)
data/lib/sidekiq/web.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "erb"
4
+ require "securerandom"
4
5
 
5
6
  require "sidekiq"
6
7
  require "sidekiq/api"
@@ -39,14 +40,21 @@ module Sidekiq
39
40
  CONTENT_SECURITY_POLICY = "Content-Security-Policy"
40
41
  LOCATION = "Location"
41
42
  X_CASCADE = "X-Cascade"
43
+ X_CONTENT_TYPE_OPTIONS = "X-Content-Type-Options"
42
44
  else
43
45
  CONTENT_LANGUAGE = "content-language"
44
46
  CONTENT_SECURITY_POLICY = "content-security-policy"
45
47
  LOCATION = "location"
46
48
  X_CASCADE = "x-cascade"
49
+ X_CONTENT_TYPE_OPTIONS = "x-content-type-options"
47
50
  end
48
51
 
49
52
  class << self
53
+ # Forward compatibility with 8.0
54
+ def configure
55
+ yield self
56
+ end
57
+
50
58
  def settings
51
59
  self
52
60
  end
@@ -114,6 +122,7 @@ module Sidekiq
114
122
  end
115
123
 
116
124
  def call(env)
125
+ env[:csp_nonce] = SecureRandom.base64(16)
117
126
  app.call(env)
118
127
  end
119
128
 
@@ -138,7 +147,50 @@ module Sidekiq
138
147
  send(:"#{attribute}=", value)
139
148
  end
140
149
 
141
- def self.register(extension)
150
+ # Register a class as a Sidekiq Web UI extension. The class should
151
+ # provide one or more tabs which map to an index route. Options:
152
+ #
153
+ # @param extension [Class] Class which contains the HTTP actions, required
154
+ # @param name [String] the name of the extension, used to namespace assets
155
+ # @param tab [String | Array] labels(s) of the UI tabs
156
+ # @param index [String | Array] index route(s) for each tab
157
+ # @param root_dir [String] directory location to find assets, locales and views, typically `web/` within the gemfile
158
+ # @param asset_paths [Array] one or more directories under {root}/assets/{name} to be publicly served, e.g. ["js", "css", "img"]
159
+ # @param cache_for [Integer] amount of time to cache assets, default one day
160
+ #
161
+ # TODO name, tab and index will be mandatory in 8.0
162
+ #
163
+ # Web extensions will have a root `web/` directory with `locales/`, `assets/`
164
+ # and `views/` subdirectories.
165
+ def self.register(extension, name: nil, tab: nil, index: nil, root_dir: nil, cache_for: 86400, asset_paths: nil)
166
+ tab = Array(tab)
167
+ index = Array(index)
168
+ tab.zip(index).each do |tab, index|
169
+ tabs[tab] = index
170
+ end
171
+ if root_dir
172
+ locdir = File.join(root_dir, "locales")
173
+ locales << locdir if File.directory?(locdir)
174
+
175
+ if asset_paths && name
176
+ # if you have {root}/assets/{name}/js/scripts.js
177
+ # and {root}/assets/{name}/css/styles.css
178
+ # you would pass in:
179
+ # asset_paths: ["js", "css"]
180
+ # See script_tag and style_tag in web/helpers.rb
181
+ assdir = File.join(root_dir, "assets")
182
+ assurls = Array(asset_paths).map { |x| "/#{name}/#{x}" }
183
+ assetprops = {
184
+ urls: assurls,
185
+ root: assdir,
186
+ cascade: true
187
+ }
188
+ assetprops[:header_rules] = [[:all, {Rack::CACHE_CONTROL => "private, max-age=#{cache_for.to_i}"}]] if cache_for
189
+ middlewares << [[Rack::Static, assetprops], nil]
190
+ end
191
+ end
192
+
193
+ yield self if block_given?
142
194
  extension.registered(WebApplication)
143
195
  end
144
196
 
@@ -166,7 +218,7 @@ module Sidekiq
166
218
  Sidekiq::WebApplication.helpers WebHelpers
167
219
  Sidekiq::WebApplication.helpers Sidekiq::Paginator
168
220
 
169
- Sidekiq::WebAction.class_eval <<-RUBY, __FILE__, __LINE__ + 1
221
+ Sidekiq::WebAction.class_eval <<-RUBY, Web::LAYOUT, -1 # standard:disable Style/EvalWithLocation
170
222
  def _render
171
223
  #{ERB.new(File.read(Web::LAYOUT)).src}
172
224
  end
data/lib/sidekiq.rb CHANGED
@@ -32,6 +32,7 @@ require "sidekiq/logger"
32
32
  require "sidekiq/client"
33
33
  require "sidekiq/transaction_aware_client"
34
34
  require "sidekiq/job"
35
+ require "sidekiq/iterable_job"
35
36
  require "sidekiq/worker_compatibility_alias"
36
37
  require "sidekiq/redis_client_adapter"
37
38
 
@@ -101,18 +102,19 @@ module Sidekiq
101
102
  def self.freeze!
102
103
  @frozen = true
103
104
  @config_blocks = nil
105
+ default_configuration.freeze!
104
106
  end
105
107
 
106
108
  # Creates a Sidekiq::Config instance that is more tuned for embedding
107
109
  # within an arbitrary Ruby process. Notably it reduces concurrency by
108
110
  # default so there is less contention for CPU time with other threads.
109
111
  #
110
- # inst = Sidekiq.configure_embed do |config|
112
+ # instance = Sidekiq.configure_embed do |config|
111
113
  # config.queues = %w[critical default low]
112
114
  # end
113
- # inst.run
115
+ # instance.run
114
116
  # sleep 10
115
- # inst.terminate
117
+ # instance.stop
116
118
  #
117
119
  # NB: it is really easy to overload a Ruby process with threads due to the GIL.
118
120
  # I do not recommend setting concurrency higher than 2-3.
data/sidekiq.gemspec CHANGED
@@ -23,8 +23,9 @@ Gem::Specification.new do |gem|
23
23
  "rubygems_mfa_required" => "true"
24
24
  }
25
25
 
26
- gem.add_dependency "redis-client", ">= 0.19.0"
26
+ gem.add_dependency "redis-client", ">= 0.22.2"
27
27
  gem.add_dependency "connection_pool", ">= 2.3.0"
28
28
  gem.add_dependency "rack", ">= 2.2.4"
29
- gem.add_dependency "concurrent-ruby", "< 2"
29
+ gem.add_dependency "logger"
30
+ gem.add_dependency "base64"
30
31
  end
@@ -34,6 +34,7 @@ function addListeners() {
34
34
  addShiftClickListeners()
35
35
  updateFuzzyTimes();
36
36
  updateNumbers();
37
+ updateProgressBars();
37
38
  setLivePollFromUrl();
38
39
 
39
40
  var buttons = document.querySelectorAll(".live-poll");
@@ -180,4 +181,8 @@ function showError(error) {
180
181
 
181
182
  function updateLocale(event) {
182
183
  event.target.form.submit();
183
- };
184
+ }
185
+
186
+ function updateProgressBars() {
187
+ document.querySelectorAll('.progress-bar').forEach(bar => { bar.style.width = bar.dataset.width + "%"})
188
+ }
@@ -83,6 +83,8 @@ class RealtimeChart extends DashboardChart {
83
83
  this.chart.data.datasets[1].data.push(failed);
84
84
  this.chart.update();
85
85
 
86
+ updateScreenReaderDashboardValues(processed, failed);
87
+
86
88
  updateStatsSummary(this.stats.sidekiq);
87
89
  updateRedisStats(this.stats.redis);
88
90
  updateFooterUTCTime(this.stats.server_utc_time);
@@ -108,17 +110,27 @@ class RealtimeChart extends DashboardChart {
108
110
  }
109
111
 
110
112
  renderLegend(dp) {
111
- this.legend.innerHTML = `
112
- <span>
113
- <span class="swatch" style="background-color: ${dp[0].dataset.borderColor};"></span>
114
- <span>${dp[0].dataset.label}: ${dp[0].formattedValue}</span>
115
- </span>
116
- <span>
117
- <span class="swatch" style="background-color: ${dp[1].dataset.borderColor};"></span>
118
- <span>${dp[1].dataset.label}: ${dp[1].formattedValue}</span>
119
- </span>
120
- <span class="time">${dp[0].label}</span>
121
- `;
113
+ const entry1 = this.legendEntry(dp[0]);
114
+ const entry2 = this.legendEntry(dp[1]);
115
+ const time = document.createElement("span");
116
+ time.classList.add("time");
117
+ time.innerText = dp[0].label;
118
+
119
+ this.legend.replaceChildren(entry1, entry2, time)
120
+ }
121
+
122
+ legendEntry(dp) {
123
+ const wrapper = document.createElement("span");
124
+
125
+ const swatch = document.createElement("span");
126
+ swatch.classList.add("swatch");
127
+ swatch.style.backgroundColor = dp.dataset.borderColor;
128
+ wrapper.appendChild(swatch)
129
+
130
+ const label = document.createElement("span");
131
+ label.innerText = `${dp.dataset.label}: ${dp.formattedValue}`;
132
+ wrapper.appendChild(label)
133
+ return wrapper;
122
134
  }
123
135
 
124
136
  renderCursor(dp) {
@@ -179,4 +191,4 @@ class RealtimeChart extends DashboardChart {
179
191
  if (hc != null) {
180
192
  var htc = new DashboardChart(hc, JSON.parse(hc.textContent))
181
193
  window.historyChart = htc
182
- }
194
+ }
@@ -28,7 +28,7 @@ var pulseBeacon = function() {
28
28
  }
29
29
 
30
30
  var setSliderLabel = function(val) {
31
- document.getElementById('sldr-text').innerText = Math.round(parseFloat(val) / 1000) + ' sec';
31
+ document.getElementById('sldr-text').innerText = Math.round(parseFloat(val) / 1000) + ' s';
32
32
  }
33
33
 
34
34
  var ready = (callback) => {
@@ -36,6 +36,12 @@ var ready = (callback) => {
36
36
  else document.addEventListener("DOMContentLoaded", callback);
37
37
  }
38
38
 
39
+ var updateScreenReaderDashboardValues = function(processed, failed) {
40
+ let lastDashboardUpdateSpan = document.getElementById("sr-last-dashboard-update");
41
+ var updateText = document.getElementById("sr-last-dashboard-update-template").innerText;
42
+ lastDashboardUpdateSpan.innerText = updateText.replace("PROCESSED_COUNT", processed).replace("FAILED_COUNT", failed);
43
+ }
44
+
39
45
  ready(() => {
40
46
  var sldr = document.getElementById('sldr');
41
47
  if (typeof localStorage.sidekiqTimeInterval !== 'undefined') {
@@ -72,6 +72,14 @@ h1, h2, h3 {
72
72
  line-height: 45px;
73
73
  }
74
74
 
75
+ .progress {
76
+ margin-bottom: 0;
77
+ }
78
+
79
+ .w-50 {
80
+ width: 50%;
81
+ }
82
+
75
83
  .header-container, .header-container .page-title-container {
76
84
  display: flex;
77
85
  justify-content: space-between;
@@ -640,18 +648,23 @@ div.interval-slider input {
640
648
 
641
649
  @media (min-width: 768px) {
642
650
  .redis-url {
643
- max-width: 250px;
651
+ max-width: 160px;
652
+ }
653
+
654
+ .navbar-fixed-bottom .nav {
655
+ margin-left: -15px;
656
+ margin-right: -15px;
644
657
  }
645
658
  }
646
659
 
647
660
  @media (min-width: 992px) {
648
661
  .redis-url {
649
- max-width: 490px;
662
+ max-width: 380px;
650
663
  }
651
664
  }
652
665
  @media (min-width: 1200px) {
653
666
  .redis-url {
654
- max-width: 600px;
667
+ max-width: 580px;
655
668
  }
656
669
  }
657
670
 
data/web/locales/en.yml CHANGED
@@ -34,6 +34,8 @@ en:
34
34
  Jobs: Jobs
35
35
  Kill: Kill
36
36
  KillAll: Kill All
37
+ Language: Language
38
+ LastDashboardUpdateTemplateLiteral: "Latest poll: Processed: PROCESSED_COUNT. Failed: FAILED_COUNT."
37
39
  LastRetry: Last Retry
38
40
  Latency: Latency
39
41
  LivePoll: Live Poll
@@ -53,6 +55,7 @@ en:
53
55
  PeakMemoryUsage: Peak Memory Usage
54
56
  Plugins: Plugins
55
57
  PollingInterval: Polling interval
58
+ PollingIntervalMilliseconds: Polling interval milliseconds
56
59
  Process: Process
57
60
  Processed: Processed
58
61
  Processes: Processes
@@ -95,7 +98,6 @@ en:
95
98
  TotalExecutionTime: Total Execution Time
96
99
  AvgExecutionTime: Average Execution Time
97
100
  Context: Context
98
- Bucket: Bucket
99
101
  NoJobMetricsFound: No recent job metrics were found
100
102
  Filter: Filter
101
103
  AnyJobContent: Any job content
data/web/locales/fr.yml CHANGED
@@ -95,5 +95,4 @@ fr:
95
95
  TotalExecutionTime: Temps d'exécution total
96
96
  AvgExecutionTime: Temps d'exécution moyen
97
97
  Context: Contexte
98
- Bucket: Bucket
99
98
  NoJobMetricsFound: Aucune statistique de tâche récente n'a été trouvée
data/web/locales/gd.yml CHANGED
@@ -95,5 +95,4 @@ gd:
95
95
  TotalExecutionTime: Ùine iomlan nan gnìomhan
96
96
  AvgExecutionTime: Ùine cuibheasach nan gnìomhan
97
97
  Context: Co-theacsa
98
- Bucket: Bucaid
99
98
  NoJobMetricsFound: Cha deach meatraigeachd o chionn goirid air obair a lorg