sidekiq 6.2.2 → 8.1.5

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 (181) hide show
  1. checksums.yaml +4 -4
  2. data/Changes.md +726 -11
  3. data/LICENSE.txt +9 -0
  4. data/README.md +70 -39
  5. data/bin/kiq +17 -0
  6. data/bin/lint-herb +13 -0
  7. data/bin/multi_queue_bench +271 -0
  8. data/bin/sidekiq +4 -9
  9. data/bin/sidekiqload +214 -115
  10. data/bin/sidekiqmon +4 -1
  11. data/bin/webload +69 -0
  12. data/lib/active_job/queue_adapters/sidekiq_adapter.rb +124 -0
  13. data/lib/generators/sidekiq/job_generator.rb +71 -0
  14. data/lib/generators/sidekiq/templates/{worker.rb.erb → job.rb.erb} +3 -3
  15. data/lib/generators/sidekiq/templates/{worker_spec.rb.erb → job_spec.rb.erb} +1 -1
  16. data/lib/generators/sidekiq/templates/{worker_test.rb.erb → job_test.rb.erb} +1 -1
  17. data/lib/sidekiq/api.rb +729 -264
  18. data/lib/sidekiq/capsule.rb +135 -0
  19. data/lib/sidekiq/cli.rb +124 -100
  20. data/lib/sidekiq/client.rb +153 -106
  21. data/lib/sidekiq/component.rb +132 -0
  22. data/lib/sidekiq/config.rb +320 -0
  23. data/lib/sidekiq/deploy.rb +64 -0
  24. data/lib/sidekiq/embedded.rb +64 -0
  25. data/lib/sidekiq/fetch.rb +27 -26
  26. data/lib/sidekiq/iterable_job.rb +56 -0
  27. data/lib/sidekiq/job/interrupt_handler.rb +24 -0
  28. data/lib/sidekiq/job/iterable/active_record_enumerator.rb +53 -0
  29. data/lib/sidekiq/job/iterable/csv_enumerator.rb +47 -0
  30. data/lib/sidekiq/job/iterable/enumerators.rb +135 -0
  31. data/lib/sidekiq/job/iterable.rb +322 -0
  32. data/lib/sidekiq/job.rb +397 -5
  33. data/lib/sidekiq/job_logger.rb +23 -32
  34. data/lib/sidekiq/job_retry.rb +141 -68
  35. data/lib/sidekiq/job_util.rb +113 -0
  36. data/lib/sidekiq/launcher.rb +122 -98
  37. data/lib/sidekiq/loader.rb +57 -0
  38. data/lib/sidekiq/logger.rb +27 -106
  39. data/lib/sidekiq/manager.rb +41 -43
  40. data/lib/sidekiq/metrics/query.rb +184 -0
  41. data/lib/sidekiq/metrics/shared.rb +109 -0
  42. data/lib/sidekiq/metrics/tracking.rb +153 -0
  43. data/lib/sidekiq/middleware/chain.rb +96 -51
  44. data/lib/sidekiq/middleware/current_attributes.rb +120 -0
  45. data/lib/sidekiq/middleware/i18n.rb +8 -4
  46. data/lib/sidekiq/middleware/modules.rb +23 -0
  47. data/lib/sidekiq/monitor.rb +16 -6
  48. data/lib/sidekiq/paginator.rb +37 -10
  49. data/lib/sidekiq/processor.rb +105 -87
  50. data/lib/sidekiq/profiler.rb +73 -0
  51. data/lib/sidekiq/rails.rb +49 -36
  52. data/lib/sidekiq/redis_client_adapter.rb +117 -0
  53. data/lib/sidekiq/redis_connection.rb +55 -86
  54. data/lib/sidekiq/ring_buffer.rb +32 -0
  55. data/lib/sidekiq/scheduled.rb +106 -50
  56. data/lib/sidekiq/systemd.rb +2 -0
  57. data/lib/sidekiq/test_api.rb +331 -0
  58. data/lib/sidekiq/testing/inline.rb +2 -30
  59. data/lib/sidekiq/testing.rb +2 -342
  60. data/lib/sidekiq/transaction_aware_client.rb +59 -0
  61. data/lib/sidekiq/tui/controls.rb +53 -0
  62. data/lib/sidekiq/tui/filtering.rb +53 -0
  63. data/lib/sidekiq/tui/tabs/base_tab.rb +204 -0
  64. data/lib/sidekiq/tui/tabs/busy.rb +118 -0
  65. data/lib/sidekiq/tui/tabs/dead.rb +19 -0
  66. data/lib/sidekiq/tui/tabs/home.rb +144 -0
  67. data/lib/sidekiq/tui/tabs/metrics.rb +131 -0
  68. data/lib/sidekiq/tui/tabs/queues.rb +95 -0
  69. data/lib/sidekiq/tui/tabs/retries.rb +19 -0
  70. data/lib/sidekiq/tui/tabs/scheduled.rb +19 -0
  71. data/lib/sidekiq/tui/tabs/set_tab.rb +96 -0
  72. data/lib/sidekiq/tui/tabs.rb +15 -0
  73. data/lib/sidekiq/tui.rb +382 -0
  74. data/lib/sidekiq/version.rb +6 -1
  75. data/lib/sidekiq/web/action.rb +149 -64
  76. data/lib/sidekiq/web/application.rb +376 -268
  77. data/lib/sidekiq/web/config.rb +117 -0
  78. data/lib/sidekiq/web/helpers.rb +213 -87
  79. data/lib/sidekiq/web/router.rb +61 -74
  80. data/lib/sidekiq/web.rb +71 -100
  81. data/lib/sidekiq/worker_compatibility_alias.rb +13 -0
  82. data/lib/sidekiq.rb +95 -196
  83. data/sidekiq.gemspec +14 -11
  84. data/web/assets/images/logo.png +0 -0
  85. data/web/assets/images/status.png +0 -0
  86. data/web/assets/javascripts/application.js +171 -57
  87. data/web/assets/javascripts/base-charts.js +120 -0
  88. data/web/assets/javascripts/chart.min.js +13 -0
  89. data/web/assets/javascripts/chartjs-adapter-date-fns.min.js +7 -0
  90. data/web/assets/javascripts/chartjs-plugin-annotation.min.js +7 -0
  91. data/web/assets/javascripts/dashboard-charts.js +194 -0
  92. data/web/assets/javascripts/dashboard.js +41 -274
  93. data/web/assets/javascripts/metrics.js +280 -0
  94. data/web/assets/stylesheets/style.css +776 -0
  95. data/web/locales/ar.yml +72 -70
  96. data/web/locales/cs.yml +64 -62
  97. data/web/locales/da.yml +62 -53
  98. data/web/locales/de.yml +67 -65
  99. data/web/locales/el.yml +45 -24
  100. data/web/locales/en.yml +93 -69
  101. data/web/locales/es.yml +91 -68
  102. data/web/locales/fa.yml +67 -65
  103. data/web/locales/fr.yml +82 -67
  104. data/web/locales/gd.yml +110 -0
  105. data/web/locales/he.yml +67 -64
  106. data/web/locales/hi.yml +61 -59
  107. data/web/locales/it.yml +94 -54
  108. data/web/locales/ja.yml +74 -68
  109. data/web/locales/ko.yml +54 -52
  110. data/web/locales/lt.yml +68 -66
  111. data/web/locales/nb.yml +63 -61
  112. data/web/locales/nl.yml +54 -52
  113. data/web/locales/pl.yml +47 -45
  114. data/web/locales/{pt-br.yml → pt-BR.yml} +85 -56
  115. data/web/locales/pt.yml +53 -51
  116. data/web/locales/ru.yml +69 -66
  117. data/web/locales/sv.yml +55 -53
  118. data/web/locales/ta.yml +62 -60
  119. data/web/locales/tr.yml +102 -0
  120. data/web/locales/uk.yml +87 -61
  121. data/web/locales/ur.yml +66 -64
  122. data/web/locales/vi.yml +69 -67
  123. data/web/locales/zh-CN.yml +107 -0
  124. data/web/locales/{zh-tw.yml → zh-TW.yml} +44 -9
  125. data/web/views/_footer.html.erb +32 -0
  126. data/web/views/_job_info.html.erb +115 -0
  127. data/web/views/_metrics_period_select.html.erb +15 -0
  128. data/web/views/_nav.html.erb +45 -0
  129. data/web/views/_paging.html.erb +26 -0
  130. data/web/views/_poll_link.html.erb +4 -0
  131. data/web/views/_summary.html.erb +40 -0
  132. data/web/views/busy.html.erb +151 -0
  133. data/web/views/dashboard.html.erb +104 -0
  134. data/web/views/dead.html.erb +38 -0
  135. data/web/views/filtering.html.erb +6 -0
  136. data/web/views/layout.html.erb +26 -0
  137. data/web/views/metrics.html.erb +85 -0
  138. data/web/views/metrics_for_job.html.erb +58 -0
  139. data/web/views/morgue.html.erb +69 -0
  140. data/web/views/profiles.html.erb +43 -0
  141. data/web/views/queue.html.erb +57 -0
  142. data/web/views/queues.html.erb +46 -0
  143. data/web/views/retries.html.erb +77 -0
  144. data/web/views/retry.html.erb +39 -0
  145. data/web/views/scheduled.html.erb +64 -0
  146. data/web/views/{scheduled_job_info.erb → scheduled_job_info.html.erb} +3 -3
  147. metadata +130 -61
  148. data/LICENSE +0 -9
  149. data/lib/generators/sidekiq/worker_generator.rb +0 -57
  150. data/lib/sidekiq/delay.rb +0 -41
  151. data/lib/sidekiq/exception_handler.rb +0 -27
  152. data/lib/sidekiq/extensions/action_mailer.rb +0 -48
  153. data/lib/sidekiq/extensions/active_record.rb +0 -43
  154. data/lib/sidekiq/extensions/class_methods.rb +0 -43
  155. data/lib/sidekiq/extensions/generic_proxy.rb +0 -33
  156. data/lib/sidekiq/util.rb +0 -95
  157. data/lib/sidekiq/web/csrf_protection.rb +0 -180
  158. data/lib/sidekiq/worker.rb +0 -244
  159. data/web/assets/stylesheets/application-dark.css +0 -147
  160. data/web/assets/stylesheets/application-rtl.css +0 -246
  161. data/web/assets/stylesheets/application.css +0 -1053
  162. data/web/assets/stylesheets/bootstrap-rtl.min.css +0 -9
  163. data/web/assets/stylesheets/bootstrap.css +0 -5
  164. data/web/locales/zh-cn.yml +0 -68
  165. data/web/views/_footer.erb +0 -20
  166. data/web/views/_job_info.erb +0 -89
  167. data/web/views/_nav.erb +0 -52
  168. data/web/views/_paging.erb +0 -23
  169. data/web/views/_poll_link.erb +0 -7
  170. data/web/views/_status.erb +0 -4
  171. data/web/views/_summary.erb +0 -40
  172. data/web/views/busy.erb +0 -132
  173. data/web/views/dashboard.erb +0 -83
  174. data/web/views/dead.erb +0 -34
  175. data/web/views/layout.erb +0 -42
  176. data/web/views/morgue.erb +0 -78
  177. data/web/views/queue.erb +0 -55
  178. data/web/views/queues.erb +0 -38
  179. data/web/views/retries.erb +0 -83
  180. data/web/views/retry.erb +0 -34
  181. data/web/views/scheduled.erb +0 -57
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sidekiq
4
+ class Web
5
+ ##
6
+ # Configure the Sidekiq::Web instance in this process:
7
+ #
8
+ # require "sidekiq/web"
9
+ # Sidekiq::Web.configure do |config|
10
+ # config.register(MyExtension, name: "myext", tab: "TabName", index: "tabpage/")
11
+ # end
12
+ #
13
+ # This should go in your `config/routes.rb` or similar. It
14
+ # does not belong in your initializer since Web should not be
15
+ # loaded in some processes (like an actual Sidekiq process).
16
+ # See `examples/webui-ext` for a sample web extension.
17
+ class Config
18
+ extend Forwardable
19
+
20
+ OPTIONS = {
21
+ # By default we support direct uploads to p.f.c since the UI is a JS SPA
22
+ # and very difficult for us to vendor or provide ourselves. If you are worried
23
+ # about data security and wish to self-host, you can change these URLs.
24
+ profile_view_url: "https://profiler.firefox.com/public/%s",
25
+ profile_store_url: "https://api.profiler.firefox.com/compressed-store"
26
+ }
27
+
28
+ ##
29
+ # Allows users to add custom rows to all of the Job
30
+ # tables, e.g. Retries, Dead, Scheduled, with custom
31
+ # links to other systems, see _job_info.erb and test
32
+ # in web_test.rb
33
+ #
34
+ # Sidekiq::Web.configure do |cfg|
35
+ # cfg.custom_job_info_rows << JobLogLink.new
36
+ # end
37
+ #
38
+ # class JobLogLink
39
+ # def add_pair(job)
40
+ # yield "External Logs", "<a href='https://example.com/logs/#{job.jid}'>Logs for #{job.jid}</a>"
41
+ # end
42
+ # end
43
+ attr_accessor :custom_job_info_rows
44
+
45
+ attr_reader :tabs
46
+ attr_reader :locales
47
+ attr_reader :views
48
+ attr_reader :middlewares
49
+
50
+ # Adds the "Back to App" link in the header
51
+ attr_accessor :app_url
52
+ attr_accessor :assets_path
53
+
54
+ def initialize
55
+ @options = OPTIONS.dup
56
+ @locales = LOCALES
57
+ @views = VIEWS
58
+ @assets_path = ASSETS
59
+ @tabs = DEFAULT_TABS.dup
60
+ @middlewares = []
61
+ @custom_job_info_rows = []
62
+ end
63
+
64
+ def_delegators :@options, :[], :[]=, :fetch, :key?, :has_key?, :merge!, :dig
65
+
66
+ def use(*args, &block)
67
+ middlewares << [args, block]
68
+ end
69
+
70
+ # Register a class as a Sidekiq Web UI extension. The class should
71
+ # provide one or more tabs which map to an index route. Options:
72
+ #
73
+ # @param extclass [Class] Class which contains the HTTP actions, required
74
+ # @param name [String] the name of the extension, used to namespace assets
75
+ # @param tab [String | Array] labels(s) of the UI tabs
76
+ # @param index [String | Array] index route(s) for each tab
77
+ # @param root_dir [String] directory location to find assets, locales and views, typically `web/` within the gemfile
78
+ # @param asset_paths [Array] one or more directories under {root}/assets/{name} to be publicly served, e.g. ["js", "css", "img"]
79
+ # @param cache_for [Integer] amount of time to cache assets, default one day
80
+ #
81
+ # Web extensions will have a root `web/` directory with `locales/`, `assets/`
82
+ # and `views/` subdirectories.
83
+ def register_extension(extclass, name:, tab:, index:, root_dir: nil, cache_for: 86400, asset_paths: nil)
84
+ tab = Array(tab)
85
+ index = Array(index)
86
+ tab.zip(index).each do |tab, index|
87
+ tabs[tab] = index
88
+ end
89
+ if root_dir
90
+ locdir = File.join(root_dir, "locales")
91
+ locales << locdir if File.directory?(locdir)
92
+
93
+ if asset_paths && name
94
+ # if you have {root}/assets/{name}/js/scripts.js
95
+ # and {root}/assets/{name}/css/styles.css
96
+ # you would pass in:
97
+ # asset_paths: ["js", "css"]
98
+ # See script_tag and style_tag in web/helpers.rb
99
+ assdir = File.join(root_dir, "assets")
100
+ assurls = Array(asset_paths).map { |x| "/#{name}/#{x}" }
101
+ assetprops = {
102
+ urls: assurls,
103
+ root: assdir,
104
+ cascade: true
105
+ }
106
+ assetprops[:header_rules] = [[:all, {"cache-control" => "private, max-age=#{cache_for.to_i}"}]] if cache_for
107
+ middlewares << [[Rack::Static, assetprops], nil]
108
+ end
109
+ end
110
+
111
+ yield self if block_given?
112
+ extclass.registered(Web::Application)
113
+ end
114
+ alias_method :register, :register_extension
115
+ end
116
+ end
117
+ end
@@ -1,26 +1,118 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "uri"
4
- require "set"
5
- require "yaml"
6
- require "cgi"
4
+ require "cgi/escape"
7
5
 
8
6
  module Sidekiq
9
- # This is not a public API
7
+ # These methods are available to pages within the Web UI and UI extensions.
8
+ # They are not public APIs for applications to use.
10
9
  module WebHelpers
10
+ def store_name
11
+ hash = redis_info
12
+ return "Dragonfly" if hash.has_key?("dragonfly_version")
13
+ return "Valkey" if hash.has_key?("valkey_version")
14
+ "Redis"
15
+ end
16
+
17
+ def store_version
18
+ hash = redis_info
19
+ return hash["dragonfly_version"] if hash.has_key?("dragonfly_version")
20
+ return hash["valkey_version"] if hash.has_key?("valkey_version")
21
+ hash["redis_version"]
22
+ end
23
+
24
+ def style_tag(location, **kwargs)
25
+ global = location.match?(/:\/\//)
26
+ location = root_path + location if !global && !location.start_with?(root_path)
27
+ attrs = {
28
+ type: "text/css",
29
+ media: "screen",
30
+ rel: "stylesheet",
31
+ nonce: csp_nonce,
32
+ href: location
33
+ }
34
+ add_to_head do
35
+ html_tag(:link, attrs.merge(kwargs))
36
+ end
37
+ end
38
+
39
+ def script_tag(location, **kwargs)
40
+ global = location.match?(/:\/\//)
41
+ location = root_path + location if !global && !location.start_with?(root_path)
42
+ attrs = {
43
+ type: "text/javascript",
44
+ nonce: csp_nonce,
45
+ src: location
46
+ }
47
+ html_tag(:script, attrs.merge(kwargs)) {}
48
+ end
49
+
50
+ # NB: keys and values are not escaped; do not allow user input
51
+ # in the attributes
52
+ private def html_tag(tagname, attrs)
53
+ s = "<#{tagname}"
54
+ attrs.each_pair do |k, v|
55
+ next unless v
56
+ s << " #{k}=\"#{v}\""
57
+ end
58
+ if block_given?
59
+ s << ">"
60
+ yield s
61
+ s << "</#{tagname}>"
62
+ else
63
+ s << " />"
64
+ end
65
+ s
66
+ end
67
+
11
68
  def strings(lang)
12
- @strings ||= {}
69
+ @@strings ||= {}
13
70
 
14
71
  # Allow sidekiq-web extensions to add locale paths
15
72
  # so extensions can be localized
16
- @strings[lang] ||= settings.locales.each_with_object({}) do |path, global|
73
+ @@strings[lang] ||= config.locales.each_with_object({}) do |path, global|
17
74
  find_locale_files(lang).each do |file|
18
- strs = YAML.load(File.open(file))
75
+ strs = parse_yaml_new(file)
19
76
  global.merge!(strs[lang])
20
77
  end
21
78
  end
22
79
  end
23
80
 
81
+ # TODO Remove
82
+ def parse_yaml_old(path)
83
+ require "yaml"
84
+ YAML.safe_load_file(path)
85
+ end
86
+
87
+ def parse_yaml_new(path)
88
+ locale = nil
89
+ map = {}
90
+ IO.readlines(path, chomp: true).each do |line|
91
+ case line
92
+ when /\A\s*\#.*/
93
+ # line comment
94
+ when !locale && /\A([a-zA-Z\-_]+):/
95
+ locale = $1
96
+ map[locale] = {}
97
+ when /\A\s+(\w+):\s+(.+)\z/
98
+ # A few values have double quotes to include special characters in YAML.
99
+ # Strip them off manually as our greedy match will include them.
100
+ key = $1
101
+ s = $2
102
+ s = s[1..] if s[0] == "\""
103
+ s = s[0..-2] if s[-1] == "\""
104
+ map[locale][key] = s
105
+ else
106
+ raise ArgumentError, "unable to parse #{path}: #{line}"
107
+ end
108
+ end
109
+ map
110
+ end
111
+
112
+ def to_json(x)
113
+ Sidekiq.dump_json(x)
114
+ end
115
+
24
116
  def singularize(str, count)
25
117
  if count == 1 && str.respond_to?(:singularize) # rails
26
118
  str.singularize
@@ -30,27 +122,52 @@ module Sidekiq
30
122
  end
31
123
 
32
124
  def clear_caches
33
- @strings = nil
34
- @locale_files = nil
35
- @available_locales = nil
125
+ @@strings = nil
126
+ @@locale_files = nil
127
+ @@available_locales = nil
36
128
  end
37
129
 
38
130
  def locale_files
39
- @locale_files ||= settings.locales.flat_map { |path|
131
+ @@locale_files ||= config.locales.flat_map { |path|
40
132
  Dir["#{path}/*.yml"]
41
133
  }
42
134
  end
43
135
 
44
136
  def available_locales
45
- @available_locales ||= locale_files.map { |path| File.basename(path, ".yml") }.uniq
137
+ @@available_locales ||= Set.new(locale_files.map { |path| File.basename(path, ".yml") })
46
138
  end
47
139
 
48
140
  def find_locale_files(lang)
49
141
  locale_files.select { |file| file =~ /\/#{lang}\.yml$/ }
50
142
  end
51
143
 
52
- # This is a hook for a Sidekiq Pro feature. Please don't touch.
53
- def filtering(*)
144
+ def language_name(locale)
145
+ strings(locale).fetch("LanguageName", locale)
146
+ end
147
+
148
+ def search(jobset, substr)
149
+ resultset = jobset.scan(substr).to_a
150
+ @current_page = 1
151
+ @count = @total_size = resultset.size
152
+ resultset
153
+ end
154
+
155
+ def filtering(which, placeholder_key: "AnyJobContent", label_key: "Filter")
156
+ erb(:filtering, locals: {which:, placeholder_key:, label_key:})
157
+ end
158
+
159
+ def filter_link(jid, within = "retries")
160
+ if within.nil?
161
+ ::Rack::Utils.escape_html(jid)
162
+ else
163
+ "<a href='#{root_path}#{within}?substr=#{jid}'>#{::Rack::Utils.escape_html(jid)}</a>"
164
+ end
165
+ end
166
+
167
+ def display_tags(job, within = "retries")
168
+ job.tags.map { |tag|
169
+ "<span class='label label-info jobtag jobtag-#{Rack::Utils.escape_html(tag)}'>#{filter_link(tag, within)}</span>"
170
+ }.join(" ")
54
171
  end
55
172
 
56
173
  # This view helper provide ability display you html code in
@@ -70,17 +187,6 @@ module Sidekiq
70
187
  @head_html.join if defined?(@head_html)
71
188
  end
72
189
 
73
- def poll_path
74
- if current_path != "" && params["poll"]
75
- path = root_path + current_path
76
- query_string = to_query_string(params.slice(*params.keys - %w[page poll]))
77
- path += "?#{query_string}" unless query_string.empty?
78
- path
79
- else
80
- ""
81
- end
82
- end
83
-
84
190
  def text_direction
85
191
  get_locale["TextDirection"] || "ltr"
86
192
  end
@@ -89,10 +195,13 @@ module Sidekiq
89
195
  text_direction == "rtl"
90
196
  end
91
197
 
92
- # See https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.4
198
+ # See https://www.rfc-editor.org/rfc/rfc9110.html#section-12.5.4
199
+ # Returns an array of language tags ordered by their quality value
200
+ #
201
+ # Inspiration taken from https://github.com/iain/http_accept_language/blob/master/lib/http_accept_language/parser.rb
93
202
  def user_preferred_languages
94
203
  languages = env["HTTP_ACCEPT_LANGUAGE"]
95
- languages.to_s.downcase.gsub(/\s+/, "").split(",").map { |language|
204
+ languages.to_s.gsub(/\s+/, "").split(",").map { |language|
96
205
  locale, quality = language.split(";q=", 2)
97
206
  locale = nil if locale == "*" # Ignore wildcards
98
207
  quality = quality ? quality.to_f : 1.0
@@ -104,34 +213,39 @@ module Sidekiq
104
213
 
105
214
  # Given an Accept-Language header like "fr-FR,fr;q=0.8,en-US;q=0.6,en;q=0.4,ru;q=0.2"
106
215
  # this method will try to best match the available locales to the user's preferred languages.
107
- #
108
- # Inspiration taken from https://github.com/iain/http_accept_language/blob/master/lib/http_accept_language/parser.rb
109
216
  def locale
110
- @locale ||= begin
111
- matched_locale = user_preferred_languages.map { |preferred|
112
- preferred_language = preferred.split("-", 2).first
217
+ # session[:locale] is set via the locale selector from the footer
218
+ @locale ||= if (l = session&.fetch(:locale, nil)) && available_locales.include?(l)
219
+ l
220
+ else
221
+ matched_locale = nil
222
+ # Attempt to find a case-insensitive exact match first
223
+ user_preferred_languages.each do |preferred|
224
+ # We only care about the language and primary subtag
225
+ # "en-GB-oxendict" becomes "en-GB"
226
+ language_tag = preferred.split("-")[0..1].join("-")
227
+ matched_locale = available_locales.find { |available_locale| available_locale.casecmp?(language_tag) }
228
+ break if matched_locale
229
+ end
113
230
 
114
- lang_group = available_locales.select { |available|
115
- preferred_language == available.split("-", 2).first
116
- }
231
+ return matched_locale if matched_locale
117
232
 
118
- lang_group.find { |lang| lang == preferred } || lang_group.min_by(&:length)
119
- }.compact.first
233
+ # Find the first base language match
234
+ # "en-US,es-MX;q=0.9" matches "en"
235
+ user_preferred_languages.each do |preferred|
236
+ base_language = preferred.split("-", 2).first
237
+ matched_locale = available_locales.find { |available_locale| available_locale.casecmp?(base_language) }
238
+ break if matched_locale
239
+ end
120
240
 
121
241
  matched_locale || "en"
122
242
  end
123
243
  end
124
244
 
125
- # within is used by Sidekiq Pro
126
- def display_tags(job, within = nil)
127
- job.tags.map { |tag|
128
- "<span class='label label-info jobtag'>#{::Rack::Utils.escape_html(tag)}</span>"
129
- }.join(" ")
130
- end
131
-
132
- # mperham/sidekiq#3243
245
+ # sidekiq/sidekiq#3243
133
246
  def unfiltered?
134
- yield unless env["PATH_INFO"].start_with?("/filter/")
247
+ s = url_params("substr")
248
+ yield unless s && s.size > 0
135
249
  end
136
250
 
137
251
  def get_locale
@@ -148,33 +262,42 @@ module Sidekiq
148
262
  end
149
263
 
150
264
  def sort_direction_label
151
- params[:direction] == "asc" ? "&uarr;" : "&darr;"
265
+ (url_params("direction") == "asc") ? "&uarr;" : "&darr;"
152
266
  end
153
267
 
154
- def workers
155
- @workers ||= Sidekiq::Workers.new
268
+ def workset
269
+ @work ||= Sidekiq::WorkSet.new
156
270
  end
157
271
 
158
272
  def processes
159
273
  @processes ||= Sidekiq::ProcessSet.new
160
274
  end
161
275
 
276
+ # Sorts processes by hostname following the natural sort order
277
+ def sorted_processes
278
+ @sorted_processes ||= begin
279
+ return processes unless processes.all? { |p| p["hostname"] }
280
+
281
+ processes.to_a.sort_by do |process|
282
+ # Kudos to `shurikk` on StackOverflow
283
+ # https://stackoverflow.com/a/15170063/575547
284
+ process["hostname"].split(/(\d+)/).map { |a| /\d+/.match?(a) ? a.to_i : a }
285
+ end
286
+ end
287
+ end
288
+
162
289
  def stats
163
290
  @stats ||= Sidekiq::Stats.new
164
291
  end
165
292
 
166
- def redis_connection
293
+ def redis_url
167
294
  Sidekiq.redis do |conn|
168
- conn.connection[:id]
295
+ conn.config.server_url
169
296
  end
170
297
  end
171
298
 
172
- def namespace
173
- @ns ||= Sidekiq.redis { |conn| conn.respond_to?(:namespace) ? conn.namespace : nil }
174
- end
175
-
176
299
  def redis_info
177
- Sidekiq.redis_info
300
+ @info ||= Sidekiq.default_configuration.redis_info
178
301
  end
179
302
 
180
303
  def root_path
@@ -186,7 +309,7 @@ module Sidekiq
186
309
  end
187
310
 
188
311
  def current_status
189
- workers.size == 0 ? "idle" : "active"
312
+ (workset.size == 0) ? "idle" : "active"
190
313
  end
191
314
 
192
315
  def relative_time(time)
@@ -194,32 +317,43 @@ module Sidekiq
194
317
  %(<time class="ltr" dir="ltr" title="#{stamp}" datetime="#{stamp}">#{time}</time>)
195
318
  end
196
319
 
320
+ def queue_names_by_capsule(pro)
321
+ cap = pro.capsules
322
+ if cap
323
+ cap.map { |k, v| v["weights"].keys.join(", ") }.join("; ")
324
+ else
325
+ # DEPRECATED Backwards compatibility with older processes.
326
+ # 'capsules' element added in v8.0.9
327
+ pro.queues.join(", ")
328
+ end
329
+ end
330
+
197
331
  def job_params(job, score)
198
332
  "#{score}-#{job["jid"]}"
199
333
  end
200
334
 
201
- def parse_params(params)
202
- score, jid = params.split("-", 2)
335
+ def parse_key(key)
336
+ score, jid = key.split("-", 2)
203
337
  [score.to_f, jid]
204
338
  end
205
339
 
206
- SAFE_QPARAMS = %w[page poll direction]
340
+ SAFE_QPARAMS = %w[page direction]
207
341
 
208
342
  # Merge options with current params, filter safe params, and stringify to query string
209
343
  def qparams(options)
210
344
  stringified_options = options.transform_keys(&:to_s)
211
345
 
212
- to_query_string(params.merge(stringified_options))
346
+ to_query_string(request.params.merge(stringified_options))
213
347
  end
214
348
 
215
- def to_query_string(params)
216
- params.map { |key, value|
349
+ def to_query_string(hash)
350
+ hash.map { |key, value|
217
351
  SAFE_QPARAMS.include?(key) ? "#{key}=#{CGI.escape(value.to_s)}" : next
218
352
  }.compact.join("&")
219
353
  end
220
354
 
221
355
  def truncate(text, truncate_after_chars = 2000)
222
- truncate_after_chars && text.size > truncate_after_chars ? "#{text[0..truncate_after_chars]}..." : text
356
+ (truncate_after_chars && text.size > truncate_after_chars) ? "#{text[0..truncate_after_chars]}..." : text
223
357
  end
224
358
 
225
359
  def display_args(args, truncate_after_chars = 2000)
@@ -236,7 +370,11 @@ module Sidekiq
236
370
  end
237
371
 
238
372
  def csrf_tag
239
- "<input type='hidden' name='authenticity_token' value='#{env[:csrf_token]}'/>"
373
+ ""
374
+ end
375
+
376
+ def csp_nonce
377
+ env[:csp_nonce]
240
378
  end
241
379
 
242
380
  def to_display(arg)
@@ -253,7 +391,7 @@ module Sidekiq
253
391
  queue class args retry_count retried_at failed_at
254
392
  jid error_message error_class backtrace
255
393
  error_backtrace enqueued_at retry wrapped
256
- created_at tags
394
+ created_at tags display_class
257
395
  ])
258
396
 
259
397
  def retry_extra_items(retry_job)
@@ -272,27 +410,17 @@ module Sidekiq
272
410
  elsif rss_kb < 10_000_000
273
411
  "#{number_with_delimiter((rss_kb / 1024.0).to_i)} MB"
274
412
  else
275
- "#{number_with_delimiter((rss_kb / (1024.0 * 1024.0)).round(1))} GB"
413
+ "#{number_with_delimiter(rss_kb / (1024.0 * 1024.0), precision: 1)} GB"
276
414
  end
277
415
  end
278
416
 
279
- def number_with_delimiter(number)
280
- return "" if number.nil?
281
-
282
- begin
283
- Float(number)
284
- rescue ArgumentError, TypeError
285
- return number
286
- end
287
-
288
- options = {delimiter: ",", separator: "."}
289
- parts = number.to_s.to_str.split(".")
290
- parts[0].gsub!(/(\d)(?=(\d\d\d)+(?!\d))/, "\\1#{options[:delimiter]}")
291
- parts.join(options[:separator])
417
+ def number_with_delimiter(number, options = {})
418
+ precision = options[:precision] || 0
419
+ %(<span data-nwp="#{precision}">#{number.round(precision)}</span>)
292
420
  end
293
421
 
294
422
  def h(text)
295
- ::Rack::Utils.escape_html(text)
423
+ ::Rack::Utils.escape_html(text.to_s)
296
424
  rescue ArgumentError => e
297
425
  raise unless e.message.eql?("invalid byte sequence in UTF-8")
298
426
  text.encode!("UTF-16", "UTF-8", invalid: :replace, replace: "").encode!("UTF-8", "UTF-16")
@@ -312,7 +440,7 @@ module Sidekiq
312
440
  end
313
441
 
314
442
  def environment_title_prefix
315
- environment = Sidekiq.options[:environment] || ENV["APP_ENV"] || ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "development"
443
+ environment = Sidekiq.default_configuration[:environment] || ENV["APP_ENV"] || ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "development"
316
444
 
317
445
  "[#{environment.upcase}] " unless environment == "production"
318
446
  end
@@ -325,11 +453,9 @@ module Sidekiq
325
453
  Time.now.utc.strftime("%H:%M:%S UTC")
326
454
  end
327
455
 
328
- def redis_connection_and_namespace
329
- @redis_connection_and_namespace ||= begin
330
- namespace_suffix = namespace.nil? ? "" : "##{namespace}"
331
- "#{redis_connection}#{namespace_suffix}"
332
- end
456
+ def pollable?
457
+ # there's no point to refreshing the metrics pages every N seconds
458
+ !(current_path == "" || current_path.index("metrics"))
333
459
  end
334
460
 
335
461
  def retry_or_delete_or_kill(job, params)