sidekiq 7.3.9 → 8.0.0.beta2

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 (102) hide show
  1. checksums.yaml +4 -4
  2. data/Changes.md +28 -0
  3. data/README.md +16 -13
  4. data/bin/sidekiqload +10 -10
  5. data/bin/webload +69 -0
  6. data/lib/active_job/queue_adapters/sidekiq_adapter.rb +5 -5
  7. data/lib/sidekiq/api.rb +120 -36
  8. data/lib/sidekiq/capsule.rb +6 -6
  9. data/lib/sidekiq/cli.rb +15 -19
  10. data/lib/sidekiq/client.rb +13 -16
  11. data/lib/sidekiq/component.rb +40 -2
  12. data/lib/sidekiq/config.rb +18 -15
  13. data/lib/sidekiq/embedded.rb +1 -0
  14. data/lib/sidekiq/iterable_job.rb +1 -0
  15. data/lib/sidekiq/job/iterable.rb +13 -4
  16. data/lib/sidekiq/job_retry.rb +17 -5
  17. data/lib/sidekiq/job_util.rb +5 -1
  18. data/lib/sidekiq/launcher.rb +1 -1
  19. data/lib/sidekiq/logger.rb +6 -10
  20. data/lib/sidekiq/manager.rb +0 -1
  21. data/lib/sidekiq/metrics/query.rb +71 -45
  22. data/lib/sidekiq/metrics/shared.rb +4 -1
  23. data/lib/sidekiq/metrics/tracking.rb +9 -7
  24. data/lib/sidekiq/middleware/current_attributes.rb +5 -17
  25. data/lib/sidekiq/paginator.rb +8 -1
  26. data/lib/sidekiq/processor.rb +21 -14
  27. data/lib/sidekiq/profiler.rb +59 -0
  28. data/lib/sidekiq/redis_client_adapter.rb +0 -1
  29. data/lib/sidekiq/testing.rb +2 -2
  30. data/lib/sidekiq/version.rb +2 -2
  31. data/lib/sidekiq/web/action.rb +104 -84
  32. data/lib/sidekiq/web/application.rb +347 -332
  33. data/lib/sidekiq/web/config.rb +116 -0
  34. data/lib/sidekiq/web/helpers.rb +41 -16
  35. data/lib/sidekiq/web/router.rb +60 -76
  36. data/lib/sidekiq/web.rb +51 -156
  37. data/lib/sidekiq.rb +1 -1
  38. data/sidekiq.gemspec +5 -4
  39. data/web/assets/javascripts/application.js +6 -13
  40. data/web/assets/javascripts/base-charts.js +30 -16
  41. data/web/assets/javascripts/chartjs-adapter-date-fns.min.js +7 -0
  42. data/web/assets/javascripts/metrics.js +16 -34
  43. data/web/assets/stylesheets/style.css +750 -0
  44. data/web/locales/ar.yml +1 -0
  45. data/web/locales/cs.yml +1 -0
  46. data/web/locales/da.yml +1 -0
  47. data/web/locales/de.yml +1 -0
  48. data/web/locales/el.yml +1 -0
  49. data/web/locales/en.yml +6 -0
  50. data/web/locales/es.yml +24 -2
  51. data/web/locales/fa.yml +1 -0
  52. data/web/locales/fr.yml +1 -0
  53. data/web/locales/gd.yml +1 -0
  54. data/web/locales/he.yml +1 -0
  55. data/web/locales/hi.yml +1 -0
  56. data/web/locales/it.yml +1 -0
  57. data/web/locales/ja.yml +1 -0
  58. data/web/locales/ko.yml +1 -0
  59. data/web/locales/lt.yml +1 -0
  60. data/web/locales/nb.yml +1 -0
  61. data/web/locales/nl.yml +1 -0
  62. data/web/locales/pl.yml +1 -0
  63. data/web/locales/{pt-br.yml → pt-BR.yml} +2 -1
  64. data/web/locales/pt.yml +1 -0
  65. data/web/locales/ru.yml +1 -0
  66. data/web/locales/sv.yml +1 -0
  67. data/web/locales/ta.yml +1 -0
  68. data/web/locales/tr.yml +1 -0
  69. data/web/locales/uk.yml +1 -0
  70. data/web/locales/ur.yml +1 -0
  71. data/web/locales/vi.yml +1 -0
  72. data/web/locales/{zh-cn.yml → zh-CN.yml} +85 -73
  73. data/web/locales/{zh-tw.yml → zh-TW.yml} +2 -1
  74. data/web/views/_footer.erb +31 -33
  75. data/web/views/_job_info.erb +91 -89
  76. data/web/views/_metrics_period_select.erb +13 -10
  77. data/web/views/_nav.erb +14 -21
  78. data/web/views/_paging.erb +23 -21
  79. data/web/views/_poll_link.erb +2 -2
  80. data/web/views/_summary.erb +16 -16
  81. data/web/views/busy.erb +124 -122
  82. data/web/views/dashboard.erb +62 -66
  83. data/web/views/dead.erb +31 -27
  84. data/web/views/filtering.erb +3 -3
  85. data/web/views/layout.erb +6 -22
  86. data/web/views/metrics.erb +75 -81
  87. data/web/views/metrics_for_job.erb +45 -46
  88. data/web/views/morgue.erb +61 -70
  89. data/web/views/profiles.erb +43 -0
  90. data/web/views/queue.erb +54 -52
  91. data/web/views/queues.erb +43 -41
  92. data/web/views/retries.erb +66 -75
  93. data/web/views/retry.erb +32 -27
  94. data/web/views/scheduled.erb +58 -54
  95. data/web/views/scheduled_job_info.erb +1 -1
  96. metadata +32 -18
  97. data/web/assets/stylesheets/application-dark.css +0 -147
  98. data/web/assets/stylesheets/application-rtl.css +0 -163
  99. data/web/assets/stylesheets/application.css +0 -759
  100. data/web/assets/stylesheets/bootstrap-rtl.min.css +0 -9
  101. data/web/assets/stylesheets/bootstrap.css +0 -5
  102. data/web/views/_status.erb +0 -4
@@ -0,0 +1,116 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "sidekiq/web/csrf_protection"
4
+
5
+ module Sidekiq
6
+ class Web
7
+ ##
8
+ # Configure the Sidekiq::Web instance in this process:
9
+ #
10
+ # require "sidekiq/web"
11
+ # Sidekiq::Web.configure do |config|
12
+ # config.register(MyExtension, name: "myext", tab: "TabName", index: "tabpage/")
13
+ # end
14
+ #
15
+ # This should go in your `config/routes.rb` or similar. It
16
+ # does not belong in your initializer since Web should not be
17
+ # loaded in some processes (like an actual Sidekiq process)
18
+ class Config
19
+ extend Forwardable
20
+
21
+ OPTIONS = {
22
+ # By default we support direct uploads to p.f.c since the UI is a JS SPA
23
+ # and very difficult for us to vendor or provide ourselves. If you are worried
24
+ # about data security and wish to self-host, you can change these URLs.
25
+ profile_view_url: "https://profiler.firefox.com/public/%s",
26
+ profile_store_url: "https://api.profiler.firefox.com/compressed-store"
27
+ }
28
+
29
+ ##
30
+ # Allows users to add custom rows to all of the Job
31
+ # tables, e.g. Retries, Dead, Scheduled, with custom
32
+ # links to other systems, see _job_info.erb and test
33
+ # in web_test.rb
34
+ #
35
+ # Sidekiq::Web.configure do |cfg|
36
+ # cfg.custom_job_info_rows << JobLogLink.new
37
+ # end
38
+ #
39
+ # class JobLogLink
40
+ # def add_pair(job)
41
+ # yield "External Logs", "<a href='https://example.com/logs/#{job.jid}'>Logs for #{job.jid}</a>"
42
+ # end
43
+ # end
44
+ attr_accessor :custom_job_info_rows
45
+
46
+ attr_reader :tabs
47
+ attr_reader :locales
48
+ attr_reader :views
49
+ attr_reader :middlewares
50
+
51
+ # Adds the "Back to App" link in the header
52
+ attr_accessor :app_url
53
+
54
+ def initialize
55
+ @options = OPTIONS.dup
56
+ @locales = LOCALES
57
+ @views = VIEWS
58
+ @tabs = DEFAULT_TABS.dup
59
+ @middlewares = [Sidekiq::Web::CsrfProtection]
60
+ @custom_job_info_rows = []
61
+ end
62
+
63
+ def_delegators :@options, :[], :[]=, :fetch, :key?, :has_key?, :merge!, :dig
64
+
65
+ def use(*args, &block)
66
+ middlewares << [args, block]
67
+ end
68
+
69
+ # Register a class as a Sidekiq Web UI extension. The class should
70
+ # provide one or more tabs which map to an index route. Options:
71
+ #
72
+ # @param extclass [Class] Class which contains the HTTP actions, required
73
+ # @param name [String] the name of the extension, used to namespace assets
74
+ # @param tab [String | Array] labels(s) of the UI tabs
75
+ # @param index [String | Array] index route(s) for each tab
76
+ # @param root_dir [String] directory location to find assets, locales and views, typically `web/` within the gemfile
77
+ # @param asset_paths [Array] one or more directories under {root}/assets/{name} to be publicly served, e.g. ["js", "css", "img"]
78
+ # @param cache_for [Integer] amount of time to cache assets, default one day
79
+ #
80
+ # Web extensions will have a root `web/` directory with `locales/`, `assets/`
81
+ # and `views/` subdirectories.
82
+ def register_extension(extclass, name:, tab:, index:, root_dir: nil, cache_for: 86400, asset_paths: nil)
83
+ tab = Array(tab)
84
+ index = Array(index)
85
+ tab.zip(index).each do |tab, index|
86
+ tabs[tab] = index
87
+ end
88
+ if root_dir
89
+ locdir = File.join(root_dir, "locales")
90
+ locales << locdir if File.directory?(locdir)
91
+
92
+ if asset_paths && name
93
+ # if you have {root}/assets/{name}/js/scripts.js
94
+ # and {root}/assets/{name}/css/styles.css
95
+ # you would pass in:
96
+ # asset_paths: ["js", "css"]
97
+ # See script_tag and style_tag in web/helpers.rb
98
+ assdir = File.join(root_dir, "assets")
99
+ assurls = Array(asset_paths).map { |x| "/#{name}/#{x}" }
100
+ assetprops = {
101
+ urls: assurls,
102
+ root: assdir,
103
+ cascade: true
104
+ }
105
+ assetprops[:header_rules] = [[:all, {"cache-control" => "private, max-age=#{cache_for.to_i}"}]] if cache_for
106
+ middlewares << [[Rack::Static, assetprops], nil]
107
+ end
108
+ end
109
+
110
+ yield self if block_given?
111
+ extclass.registered(Web::Application)
112
+ end
113
+ alias_method :register, :register_extension
114
+ end
115
+ end
116
+ end
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "uri"
4
- require "set"
5
4
  require "yaml"
6
5
  require "cgi"
7
6
 
@@ -9,6 +8,20 @@ module Sidekiq
9
8
  # These methods are available to pages within the Web UI and UI extensions.
10
9
  # They are not public APIs for applications to use.
11
10
  module WebHelpers
11
+ def store_name
12
+ hash = redis_info
13
+ return "Dragonfly" if hash.has_key?("dragonfly_version")
14
+ return "Valkey" if hash.has_key?("valkey_version")
15
+ "Redis"
16
+ end
17
+
18
+ def store_version
19
+ hash = redis_info
20
+ return hash["dragonfly_version"] if hash.has_key?("dragonfly_version")
21
+ return hash["valkey_version"] if hash.has_key?("valkey_version")
22
+ hash["redis_version"]
23
+ end
24
+
12
25
  def style_tag(location, **kwargs)
13
26
  global = location.match?(/:\/\//)
14
27
  location = root_path + location if !global && !location.start_with?(root_path)
@@ -19,7 +32,9 @@ module Sidekiq
19
32
  nonce: csp_nonce,
20
33
  href: location
21
34
  }
22
- html_tag(:link, attrs.merge(kwargs))
35
+ add_to_head do
36
+ html_tag(:link, attrs.merge(kwargs))
37
+ end
23
38
  end
24
39
 
25
40
  def script_tag(location, **kwargs)
@@ -36,7 +51,7 @@ module Sidekiq
36
51
  # NB: keys and values are not escaped; do not allow user input
37
52
  # in the attributes
38
53
  private def html_tag(tagname, attrs)
39
- s = +"<#{tagname}"
54
+ s = "<#{tagname}"
40
55
  attrs.each_pair do |k, v|
41
56
  next unless v
42
57
  s << " #{k}=\"#{v}\""
@@ -56,9 +71,9 @@ module Sidekiq
56
71
 
57
72
  # Allow sidekiq-web extensions to add locale paths
58
73
  # so extensions can be localized
59
- @@strings[lang] ||= settings.locales.each_with_object({}) do |path, global|
74
+ @@strings[lang] ||= config.locales.each_with_object({}) do |path, global|
60
75
  find_locale_files(lang).each do |file|
61
- strs = YAML.safe_load(File.read(file))
76
+ strs = YAML.safe_load_file(file)
62
77
  global.merge!(strs[lang])
63
78
  end
64
79
  end
@@ -83,7 +98,7 @@ module Sidekiq
83
98
  end
84
99
 
85
100
  def locale_files
86
- @@locale_files ||= settings.locales.flat_map { |path|
101
+ @@locale_files ||= config.locales.flat_map { |path|
87
102
  Dir["#{path}/*.yml"]
88
103
  }
89
104
  end
@@ -96,6 +111,10 @@ module Sidekiq
96
111
  locale_files.select { |file| file =~ /\/#{lang}\.yml$/ }
97
112
  end
98
113
 
114
+ def language_name(locale)
115
+ strings(locale).fetch("LanguageName", locale)
116
+ end
117
+
99
118
  def search(jobset, substr)
100
119
  resultset = jobset.scan(substr).to_a
101
120
  @current_page = 1
@@ -117,7 +136,7 @@ module Sidekiq
117
136
 
118
137
  def display_tags(job, within = "retries")
119
138
  job.tags.map { |tag|
120
- "<span class='label label-info jobtag'>#{filter_link(tag, within)}</span>"
139
+ "<span class='label label-info jobtag jobtag-#{Rack::Utils.escape_html(tag)}'>#{filter_link(tag, within)}</span>"
121
140
  }.join(" ")
122
141
  end
123
142
 
@@ -149,7 +168,7 @@ module Sidekiq
149
168
  # See https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.4
150
169
  def user_preferred_languages
151
170
  languages = env["HTTP_ACCEPT_LANGUAGE"]
152
- languages.to_s.downcase.gsub(/\s+/, "").split(",").map { |language|
171
+ languages.to_s.gsub(/\s+/, "").split(",").map { |language|
153
172
  locale, quality = language.split(";q=", 2)
154
173
  locale = nil if locale == "*" # Ignore wildcards
155
174
  quality = quality ? quality.to_f : 1.0
@@ -168,7 +187,13 @@ module Sidekiq
168
187
  @locale ||= if (l = session&.fetch(:locale, nil)) && available_locales.include?(l)
169
188
  l
170
189
  else
171
- matched_locale = user_preferred_languages.map { |preferred|
190
+
191
+ # exactly match with preferred like "pt-BR, zh-CN, zh-TW..." first
192
+ matched_locale = user_preferred_languages.find { |preferred|
193
+ available_locales.include?(preferred) if preferred.length == 5
194
+ }
195
+
196
+ matched_locale ||= user_preferred_languages.map { |preferred|
172
197
  preferred_language = preferred.split("-", 2).first
173
198
 
174
199
  lang_group = available_locales.select { |available|
@@ -202,7 +227,7 @@ module Sidekiq
202
227
  end
203
228
 
204
229
  def sort_direction_label
205
- (params[:direction] == "asc") ? "&uarr;" : "&darr;"
230
+ (url_params("direction") == "asc") ? "&uarr;" : "&darr;"
206
231
  end
207
232
 
208
233
  def workset
@@ -245,7 +270,7 @@ module Sidekiq
245
270
  end
246
271
 
247
272
  def redis_info
248
- Sidekiq.default_configuration.redis_info
273
+ @info ||= Sidekiq.default_configuration.redis_info
249
274
  end
250
275
 
251
276
  def root_path
@@ -269,8 +294,8 @@ module Sidekiq
269
294
  "#{score}-#{job["jid"]}"
270
295
  end
271
296
 
272
- def parse_params(params)
273
- score, jid = params.split("-", 2)
297
+ def parse_key(key)
298
+ score, jid = key.split("-", 2)
274
299
  [score.to_f, jid]
275
300
  end
276
301
 
@@ -280,11 +305,11 @@ module Sidekiq
280
305
  def qparams(options)
281
306
  stringified_options = options.transform_keys(&:to_s)
282
307
 
283
- to_query_string(params.merge(stringified_options))
308
+ to_query_string(request.params.merge(stringified_options))
284
309
  end
285
310
 
286
- def to_query_string(params)
287
- params.map { |key, value|
311
+ def to_query_string(hash)
312
+ hash.map { |key, value|
288
313
  SAFE_QPARAMS.include?(key) ? "#{key}=#{CGI.escape(value.to_s)}" : next
289
314
  }.compact.join("&")
290
315
  end
@@ -3,104 +3,88 @@
3
3
  require "rack"
4
4
 
5
5
  module Sidekiq
6
- module WebRouter
7
- GET = "GET"
8
- DELETE = "DELETE"
9
- POST = "POST"
10
- PUT = "PUT"
11
- PATCH = "PATCH"
12
- HEAD = "HEAD"
13
-
14
- ROUTE_PARAMS = "rack.route_params"
15
- REQUEST_METHOD = "REQUEST_METHOD"
16
- PATH_INFO = "PATH_INFO"
17
-
18
- def head(path, &block)
19
- route(HEAD, path, &block)
20
- end
6
+ class Web
7
+ # Provides an API to declare endpoints, along with a match
8
+ # API to dynamically route a request to an endpoint.
9
+ module Router
10
+ def head(path, &) = route(:head, path, &)
21
11
 
22
- def get(path, &block)
23
- route(GET, path, &block)
24
- end
12
+ def get(path, &) = route(:get, path, &)
25
13
 
26
- def post(path, &block)
27
- route(POST, path, &block)
28
- end
14
+ def post(path, &) = route(:post, path, &)
29
15
 
30
- def put(path, &block)
31
- route(PUT, path, &block)
32
- end
16
+ def put(path, &) = route(:put, path, &)
33
17
 
34
- def patch(path, &block)
35
- route(PATCH, path, &block)
36
- end
18
+ def patch(path, &) = route(:patch, path, &)
37
19
 
38
- def delete(path, &block)
39
- route(DELETE, path, &block)
40
- end
20
+ def delete(path, &) = route(:delete, path, &)
41
21
 
42
- def route(*methods, path, &block)
43
- @routes ||= {GET => [], POST => [], PUT => [], PATCH => [], DELETE => [], HEAD => []}
44
-
45
- methods.each do |method|
46
- method = method.to_s.upcase
47
- @routes[method] << WebRoute.new(method, path, block)
22
+ def route(*methods, path, &block)
23
+ methods.each do |method|
24
+ raise ArgumentError, "Invalid method #{method}. Must be one of #{@routes.keys.join(",")}" unless route_cache.has_key?(method)
25
+ route_cache[method] << Route.new(method, path, block)
26
+ end
48
27
  end
49
- end
50
28
 
51
- def match(env)
52
- request_method = env[REQUEST_METHOD]
53
- path_info = ::Rack::Utils.unescape env[PATH_INFO]
29
+ def match(env)
30
+ request_method = env["REQUEST_METHOD"].downcase.to_sym
31
+ path_info = ::Rack::Utils.unescape_path env["PATH_INFO"]
54
32
 
55
- # There are servers which send an empty string when requesting the root.
56
- # These servers should be ashamed of themselves.
57
- path_info = "/" if path_info == ""
33
+ # There are servers which send an empty string when requesting the root.
34
+ # These servers should be ashamed of themselves.
35
+ path_info = "/" if path_info == ""
58
36
 
59
- @routes[request_method].each do |route|
60
- params = route.match(request_method, path_info)
61
- if params
62
- env[ROUTE_PARAMS] = params
63
-
64
- return WebAction.new(env, route.block)
37
+ route_cache[request_method].each do |route|
38
+ params = route.match(request_method, path_info)
39
+ if params
40
+ env["rack.route_params"] = params
41
+ return Action.new(env, route.block)
42
+ end
65
43
  end
44
+
45
+ nil
66
46
  end
67
47
 
68
- nil
48
+ def route_cache
49
+ @@routes ||= {get: [], post: [], put: [], patch: [], delete: [], head: []}
50
+ end
69
51
  end
70
- end
71
52
 
72
- class WebRoute
73
- attr_accessor :request_method, :pattern, :block, :name
53
+ class Route
54
+ attr_accessor :request_method, :pattern, :block, :name
74
55
 
75
- NAMED_SEGMENTS_PATTERN = /\/([^\/]*):([^.:$\/]+)/
56
+ NAMED_SEGMENTS_PATTERN = /\/([^\/]*):([^.:$\/]+)/
76
57
 
77
- def initialize(request_method, pattern, block)
78
- @request_method = request_method
79
- @pattern = pattern
80
- @block = block
81
- end
58
+ def initialize(request_method, pattern, block)
59
+ @request_method = request_method
60
+ @pattern = pattern
61
+ @block = block
62
+ end
82
63
 
83
- def matcher
84
- @matcher ||= compile
85
- end
64
+ def matcher
65
+ @matcher ||= compile
66
+ end
86
67
 
87
- def compile
88
- if pattern.match?(NAMED_SEGMENTS_PATTERN)
89
- p = pattern.gsub(NAMED_SEGMENTS_PATTERN, '/\1(?<\2>[^$/]+)')
68
+ def compile
69
+ if pattern.match?(NAMED_SEGMENTS_PATTERN)
70
+ p = pattern.gsub(NAMED_SEGMENTS_PATTERN, '/\1(?<\2>[^$/]+)')
90
71
 
91
- Regexp.new("\\A#{p}\\Z")
92
- else
93
- pattern
72
+ Regexp.new("\\A#{p}\\Z")
73
+ else
74
+ pattern
75
+ end
94
76
  end
95
- end
96
77
 
97
- def match(request_method, path)
98
- case matcher
99
- when String
100
- {} if path == matcher
101
- else
102
- path_match = path.match(matcher)
103
- path_match&.named_captures&.transform_keys(&:to_sym)
78
+ EMPTY = {}.freeze
79
+
80
+ def match(request_method, path)
81
+ case matcher
82
+ when String
83
+ EMPTY if path == matcher
84
+ else
85
+ path_match = path.match(matcher)
86
+ path_match&.named_captures&.transform_keys(&:to_sym)
87
+ end
104
88
  end
105
89
  end
106
90
  end