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.
- checksums.yaml +4 -4
- data/Changes.md +116 -0
- data/README.md +1 -1
- data/bin/sidekiqload +21 -12
- data/lib/active_job/queue_adapters/sidekiq_adapter.rb +75 -0
- data/lib/generators/sidekiq/job_generator.rb +2 -0
- data/lib/sidekiq/api.rb +63 -34
- data/lib/sidekiq/capsule.rb +8 -3
- data/lib/sidekiq/cli.rb +2 -1
- data/lib/sidekiq/client.rb +21 -1
- data/lib/sidekiq/component.rb +22 -0
- data/lib/sidekiq/config.rb +27 -3
- data/lib/sidekiq/deploy.rb +2 -0
- data/lib/sidekiq/embedded.rb +2 -0
- data/lib/sidekiq/fetch.rb +1 -1
- data/lib/sidekiq/iterable_job.rb +55 -0
- data/lib/sidekiq/job/interrupt_handler.rb +24 -0
- data/lib/sidekiq/job/iterable/active_record_enumerator.rb +53 -0
- data/lib/sidekiq/job/iterable/csv_enumerator.rb +47 -0
- data/lib/sidekiq/job/iterable/enumerators.rb +135 -0
- data/lib/sidekiq/job/iterable.rb +294 -0
- data/lib/sidekiq/job.rb +13 -2
- data/lib/sidekiq/job_logger.rb +7 -6
- data/lib/sidekiq/job_retry.rb +6 -1
- data/lib/sidekiq/job_util.rb +2 -0
- data/lib/sidekiq/launcher.rb +1 -1
- data/lib/sidekiq/metrics/query.rb +2 -0
- data/lib/sidekiq/metrics/shared.rb +15 -4
- data/lib/sidekiq/metrics/tracking.rb +13 -5
- data/lib/sidekiq/middleware/current_attributes.rb +46 -13
- data/lib/sidekiq/middleware/modules.rb +2 -0
- data/lib/sidekiq/monitor.rb +2 -1
- data/lib/sidekiq/paginator.rb +6 -0
- data/lib/sidekiq/processor.rb +20 -10
- data/lib/sidekiq/rails.rb +12 -0
- data/lib/sidekiq/redis_client_adapter.rb +8 -5
- data/lib/sidekiq/redis_connection.rb +33 -2
- data/lib/sidekiq/ring_buffer.rb +2 -0
- data/lib/sidekiq/systemd.rb +2 -0
- data/lib/sidekiq/testing.rb +5 -5
- data/lib/sidekiq/version.rb +5 -1
- data/lib/sidekiq/web/action.rb +21 -4
- data/lib/sidekiq/web/application.rb +43 -82
- data/lib/sidekiq/web/helpers.rb +62 -15
- data/lib/sidekiq/web/router.rb +5 -2
- data/lib/sidekiq/web.rb +54 -2
- data/lib/sidekiq.rb +5 -3
- data/sidekiq.gemspec +3 -2
- data/web/assets/javascripts/application.js +6 -1
- data/web/assets/javascripts/dashboard-charts.js +24 -12
- data/web/assets/javascripts/dashboard.js +7 -1
- data/web/assets/stylesheets/application.css +16 -3
- data/web/locales/en.yml +3 -1
- data/web/locales/fr.yml +0 -1
- data/web/locales/gd.yml +0 -1
- data/web/locales/it.yml +32 -1
- data/web/locales/ja.yml +0 -1
- data/web/locales/pt-br.yml +1 -2
- data/web/locales/tr.yml +100 -0
- data/web/locales/uk.yml +24 -1
- data/web/locales/zh-cn.yml +0 -1
- data/web/locales/zh-tw.yml +0 -1
- data/web/views/_footer.erb +1 -2
- data/web/views/dashboard.erb +10 -7
- data/web/views/filtering.erb +1 -2
- data/web/views/layout.erb +6 -6
- data/web/views/metrics.erb +7 -8
- data/web/views/metrics_for_job.erb +4 -4
- data/web/views/morgue.erb +2 -2
- data/web/views/queue.erb +1 -1
- 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 | 
            -
                 | 
| 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'  | 
| 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 | 
            -
                   | 
| 157 | 
            -
             | 
| 158 | 
            -
                   | 
| 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  | 
| 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 | 
| 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 | 
            -
                   | 
| 211 | 
            -
             | 
| 212 | 
            -
                   | 
| 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 | 
            -
                   | 
| 266 | 
            -
             | 
| 267 | 
            -
                   | 
| 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 =>  | 
| 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)
         | 
    
        data/lib/sidekiq/web/helpers.rb
    CHANGED
    
    | @@ -6,14 +6,57 @@ require "yaml" | |
| 6 6 | 
             
            require "cgi"
         | 
| 7 7 |  | 
| 8 8 | 
             
            module Sidekiq
         | 
| 9 | 
            -
              #  | 
| 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 | 
            -
                   | 
| 55 | 
            +
                  @@strings ||= {}
         | 
| 13 56 |  | 
| 14 57 | 
             
                  # Allow sidekiq-web extensions to add locale paths
         | 
| 15 58 | 
             
                  # so extensions can be localized
         | 
| 16 | 
            -
                   | 
| 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 | 
            -
                   | 
| 38 | 
            -
                   | 
| 39 | 
            -
                   | 
| 80 | 
            +
                  @@strings = nil
         | 
| 81 | 
            +
                  @@locale_files = nil
         | 
| 82 | 
            +
                  @@available_locales = nil
         | 
| 40 83 | 
             
                end
         | 
| 41 84 |  | 
| 42 85 | 
             
                def locale_files
         | 
| 43 | 
            -
                   | 
| 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 | 
            -
                   | 
| 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} | 
| 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 | 
            -
                   | 
| 126 | 
            -
             | 
| 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 | 
            -
                   | 
| 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")
         | 
    
        data/lib/sidekiq/web/router.rb
    CHANGED
    
    | @@ -39,10 +39,13 @@ module Sidekiq | |
| 39 39 | 
             
                  route(DELETE, path, &block)
         | 
| 40 40 | 
             
                end
         | 
| 41 41 |  | 
| 42 | 
            -
                def route( | 
| 42 | 
            +
                def route(*methods, path, &block)
         | 
| 43 43 | 
             
                  @routes ||= {GET => [], POST => [], PUT => [], PATCH => [], DELETE => [], HEAD => []}
         | 
| 44 44 |  | 
| 45 | 
            -
                   | 
| 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 | 
            -
                 | 
| 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,  | 
| 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 | 
            -
              #    | 
| 112 | 
            +
              #   instance = Sidekiq.configure_embed do |config|
         | 
| 111 113 | 
             
              #     config.queues = %w[critical default low]
         | 
| 112 114 | 
             
              #   end
         | 
| 113 | 
            -
              #    | 
| 115 | 
            +
              #   instance.run
         | 
| 114 116 | 
             
              #   sleep 10
         | 
| 115 | 
            -
              #    | 
| 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. | 
| 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 " | 
| 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 | 
            -
                 | 
| 112 | 
            -
             | 
| 113 | 
            -
             | 
| 114 | 
            -
             | 
| 115 | 
            -
             | 
| 116 | 
            -
             | 
| 117 | 
            -
             | 
| 118 | 
            -
             | 
| 119 | 
            -
             | 
| 120 | 
            -
             | 
| 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) + '  | 
| 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:  | 
| 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:  | 
| 662 | 
            +
                max-width: 380px;
         | 
| 650 663 | 
             
              }
         | 
| 651 664 | 
             
            }
         | 
| 652 665 | 
             
            @media (min-width: 1200px) {
         | 
| 653 666 | 
             
              .redis-url {
         | 
| 654 | 
            -
                max-width:  | 
| 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