sidekiq 6.5.10 → 8.1.7

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 (177) hide show
  1. checksums.yaml +4 -4
  2. data/Changes.md +568 -18
  3. data/README.md +66 -38
  4. data/bin/kiq +17 -0
  5. data/bin/lint-herb +13 -0
  6. data/bin/multi_queue_bench +271 -0
  7. data/bin/sidekiq +3 -8
  8. data/bin/sidekiqload +215 -118
  9. data/bin/sidekiqmon +3 -0
  10. data/bin/webload +69 -0
  11. data/lib/active_job/queue_adapters/sidekiq_adapter.rb +124 -0
  12. data/lib/generators/sidekiq/job_generator.rb +17 -3
  13. data/lib/generators/sidekiq/templates/job.rb.erb +1 -1
  14. data/lib/sidekiq/api.rb +526 -218
  15. data/lib/sidekiq/capsule.rb +135 -0
  16. data/lib/sidekiq/cli.rb +88 -96
  17. data/lib/sidekiq/client.rb +114 -47
  18. data/lib/sidekiq/component.rb +71 -4
  19. data/lib/sidekiq/config.rb +316 -0
  20. data/lib/sidekiq/deploy.rb +64 -0
  21. data/lib/sidekiq/embedded.rb +64 -0
  22. data/lib/sidekiq/fetch.rb +12 -14
  23. data/lib/sidekiq/iterable_job.rb +56 -0
  24. data/lib/sidekiq/job/interrupt_handler.rb +24 -0
  25. data/lib/sidekiq/job/iterable/active_record_enumerator.rb +53 -0
  26. data/lib/sidekiq/job/iterable/csv_enumerator.rb +47 -0
  27. data/lib/sidekiq/job/iterable/enumerators.rb +135 -0
  28. data/lib/sidekiq/job/iterable.rb +322 -0
  29. data/lib/sidekiq/job.rb +397 -10
  30. data/lib/sidekiq/job_logger.rb +14 -11
  31. data/lib/sidekiq/job_retry.rb +78 -28
  32. data/lib/sidekiq/job_util.rb +58 -16
  33. data/lib/sidekiq/launcher.rb +86 -70
  34. data/lib/sidekiq/loader.rb +57 -0
  35. data/lib/sidekiq/logger.rb +25 -94
  36. data/lib/sidekiq/manager.rb +11 -13
  37. data/lib/sidekiq/metrics/query.rb +84 -48
  38. data/lib/sidekiq/metrics/shared.rb +29 -14
  39. data/lib/sidekiq/metrics/tracking.rb +49 -30
  40. data/lib/sidekiq/middleware/chain.rb +19 -18
  41. data/lib/sidekiq/middleware/current_attributes.rb +78 -21
  42. data/lib/sidekiq/middleware/i18n.rb +2 -0
  43. data/lib/sidekiq/middleware/modules.rb +2 -0
  44. data/lib/sidekiq/monitor.rb +15 -5
  45. data/lib/sidekiq/paginator.rb +26 -4
  46. data/lib/sidekiq/processor.rb +78 -65
  47. data/lib/sidekiq/profiler.rb +73 -0
  48. data/lib/sidekiq/rails.rb +45 -60
  49. data/lib/sidekiq/redis_client_adapter.rb +35 -72
  50. data/lib/sidekiq/redis_connection.rb +54 -114
  51. data/lib/sidekiq/ring_buffer.rb +3 -0
  52. data/lib/sidekiq/scheduled.rb +28 -30
  53. data/lib/sidekiq/systemd.rb +2 -0
  54. data/lib/sidekiq/test_api.rb +331 -0
  55. data/lib/sidekiq/testing/inline.rb +2 -30
  56. data/lib/sidekiq/testing.rb +2 -342
  57. data/lib/sidekiq/transaction_aware_client.rb +23 -9
  58. data/lib/sidekiq/tui/controls.rb +53 -0
  59. data/lib/sidekiq/tui/filtering.rb +53 -0
  60. data/lib/sidekiq/tui/tabs/base_tab.rb +230 -0
  61. data/lib/sidekiq/tui/tabs/busy.rb +104 -0
  62. data/lib/sidekiq/tui/tabs/dead.rb +14 -0
  63. data/lib/sidekiq/tui/tabs/home.rb +115 -0
  64. data/lib/sidekiq/tui/tabs/metrics.rb +114 -0
  65. data/lib/sidekiq/tui/tabs/queues.rb +83 -0
  66. data/lib/sidekiq/tui/tabs/retries.rb +14 -0
  67. data/lib/sidekiq/tui/tabs/scheduled.rb +14 -0
  68. data/lib/sidekiq/tui/tabs/set_tab.rb +91 -0
  69. data/lib/sidekiq/tui/tabs.rb +15 -0
  70. data/lib/sidekiq/tui.rb +390 -0
  71. data/lib/sidekiq/version.rb +6 -1
  72. data/lib/sidekiq/web/action.rb +149 -64
  73. data/lib/sidekiq/web/application.rb +379 -281
  74. data/lib/sidekiq/web/config.rb +117 -0
  75. data/lib/sidekiq/web/helpers.rb +190 -71
  76. data/lib/sidekiq/web/router.rb +62 -75
  77. data/lib/sidekiq/web.rb +68 -101
  78. data/lib/sidekiq/worker_compatibility_alias.rb +13 -0
  79. data/lib/sidekiq.rb +85 -269
  80. data/sidekiq.gemspec +15 -11
  81. data/web/assets/images/logo.png +0 -0
  82. data/web/assets/images/status.png +0 -0
  83. data/web/assets/javascripts/application.js +84 -24
  84. data/web/assets/javascripts/base-charts.js +120 -0
  85. data/web/assets/javascripts/chartjs-adapter-date-fns.min.js +7 -0
  86. data/web/assets/javascripts/dashboard-charts.js +194 -0
  87. data/web/assets/javascripts/dashboard.js +18 -234
  88. data/web/assets/javascripts/metrics.js +138 -120
  89. data/web/assets/stylesheets/style.css +776 -0
  90. data/web/locales/ar.yml +72 -70
  91. data/web/locales/cs.yml +64 -62
  92. data/web/locales/da.yml +62 -53
  93. data/web/locales/de.yml +67 -65
  94. data/web/locales/el.yml +4 -7
  95. data/web/locales/en.yml +89 -71
  96. data/web/locales/es.yml +91 -68
  97. data/web/locales/fa.yml +67 -65
  98. data/web/locales/fr.yml +82 -67
  99. data/web/locales/gd.yml +110 -0
  100. data/web/locales/he.yml +67 -64
  101. data/web/locales/hi.yml +95 -59
  102. data/web/locales/it.yml +94 -54
  103. data/web/locales/ja.yml +69 -70
  104. data/web/locales/ko.yml +54 -52
  105. data/web/locales/lt.yml +68 -66
  106. data/web/locales/nb.yml +63 -61
  107. data/web/locales/nl.yml +54 -52
  108. data/web/locales/pl.yml +47 -45
  109. data/web/locales/{pt-br.yml → pt-BR.yml} +81 -70
  110. data/web/locales/pt.yml +53 -51
  111. data/web/locales/ru.yml +69 -66
  112. data/web/locales/sv.yml +55 -53
  113. data/web/locales/ta.yml +100 -64
  114. data/web/locales/tr.yml +102 -0
  115. data/web/locales/uk.yml +87 -61
  116. data/web/locales/ur.yml +66 -64
  117. data/web/locales/vi.yml +69 -67
  118. data/web/locales/zh-CN.yml +107 -0
  119. data/web/locales/{zh-tw.yml → zh-TW.yml} +13 -3
  120. data/web/views/_footer.html.erb +32 -0
  121. data/web/views/_job_info.html.erb +115 -0
  122. data/web/views/_metrics_period_select.html.erb +15 -0
  123. data/web/views/_nav.html.erb +45 -0
  124. data/web/views/_paging.html.erb +26 -0
  125. data/web/views/_poll_link.html.erb +4 -0
  126. data/web/views/_summary.html.erb +40 -0
  127. data/web/views/busy.html.erb +155 -0
  128. data/web/views/dashboard.html.erb +104 -0
  129. data/web/views/dead.html.erb +38 -0
  130. data/web/views/filtering.html.erb +6 -0
  131. data/web/views/layout.html.erb +26 -0
  132. data/web/views/metrics.html.erb +85 -0
  133. data/web/views/metrics_for_job.html.erb +58 -0
  134. data/web/views/morgue.html.erb +69 -0
  135. data/web/views/profiles.html.erb +43 -0
  136. data/web/views/queue.html.erb +57 -0
  137. data/web/views/queues.html.erb +46 -0
  138. data/web/views/retries.html.erb +77 -0
  139. data/web/views/retry.html.erb +39 -0
  140. data/web/views/scheduled.html.erb +64 -0
  141. data/web/views/{scheduled_job_info.erb → scheduled_job_info.html.erb} +3 -3
  142. metadata +113 -70
  143. data/lib/sidekiq/delay.rb +0 -43
  144. data/lib/sidekiq/extensions/action_mailer.rb +0 -48
  145. data/lib/sidekiq/extensions/active_record.rb +0 -43
  146. data/lib/sidekiq/extensions/class_methods.rb +0 -43
  147. data/lib/sidekiq/extensions/generic_proxy.rb +0 -33
  148. data/lib/sidekiq/metrics/deploy.rb +0 -47
  149. data/lib/sidekiq/web/csrf_protection.rb +0 -180
  150. data/lib/sidekiq/worker.rb +0 -370
  151. data/web/assets/javascripts/graph.js +0 -16
  152. data/web/assets/stylesheets/application-dark.css +0 -143
  153. data/web/assets/stylesheets/application-rtl.css +0 -242
  154. data/web/assets/stylesheets/application.css +0 -999
  155. data/web/assets/stylesheets/bootstrap-rtl.min.css +0 -9
  156. data/web/assets/stylesheets/bootstrap.css +0 -5
  157. data/web/locales/zh-cn.yml +0 -93
  158. data/web/views/_footer.erb +0 -20
  159. data/web/views/_job_info.erb +0 -89
  160. data/web/views/_nav.erb +0 -52
  161. data/web/views/_paging.erb +0 -23
  162. data/web/views/_poll_link.erb +0 -4
  163. data/web/views/_status.erb +0 -4
  164. data/web/views/_summary.erb +0 -40
  165. data/web/views/busy.erb +0 -137
  166. data/web/views/dashboard.erb +0 -84
  167. data/web/views/dead.erb +0 -34
  168. data/web/views/layout.erb +0 -42
  169. data/web/views/metrics.erb +0 -69
  170. data/web/views/metrics_for_job.erb +0 -87
  171. data/web/views/morgue.erb +0 -78
  172. data/web/views/queue.erb +0 -59
  173. data/web/views/queues.erb +0 -38
  174. data/web/views/retries.erb +0 -83
  175. data/web/views/retry.erb +0 -34
  176. data/web/views/scheduled.erb +0 -57
  177. /data/{LICENSE → LICENSE.txt} +0 -0
@@ -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,112 @@
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.safe_load(File.open(file))
75
+ strs = parse_yaml(file)
19
76
  global.merge!(strs[lang])
20
77
  end
21
78
  end
22
79
  end
23
80
 
81
+ def parse_yaml(path)
82
+ locale = nil
83
+ map = {}
84
+ IO.readlines(path, chomp: true).each do |line|
85
+ case line
86
+ when /\A\s*\#.*/
87
+ # line comment
88
+ when !locale && /\A([a-zA-Z\-_]+):/
89
+ locale = $1
90
+ map[locale] = {}
91
+ when /\A\s+(\w+):\s+(.+)\z/
92
+ # A few values have double quotes to include special characters in YAML.
93
+ # Strip them off manually as our greedy match will include them.
94
+ key = $1
95
+ s = $2
96
+ s = s[1..] if s[0] == "\""
97
+ s = s[0..-2] if s[-1] == "\""
98
+ map[locale][key] = s
99
+ else
100
+ raise ArgumentError, "unable to parse #{path}: #{line}"
101
+ end
102
+ end
103
+ map
104
+ end
105
+
106
+ def to_json(x)
107
+ Sidekiq.dump_json(x)
108
+ end
109
+
24
110
  def singularize(str, count)
25
111
  if count == 1 && str.respond_to?(:singularize) # rails
26
112
  str.singularize
@@ -30,27 +116,53 @@ module Sidekiq
30
116
  end
31
117
 
32
118
  def clear_caches
33
- @strings = nil
34
- @locale_files = nil
35
- @available_locales = nil
119
+ @@strings = nil
120
+ @@locale_files = nil
121
+ @@available_locales = nil
36
122
  end
37
123
 
38
124
  def locale_files
39
- @locale_files ||= settings.locales.flat_map { |path|
125
+ @@locale_files ||= config.locales.flat_map { |path|
40
126
  Dir["#{path}/*.yml"]
41
127
  }
42
128
  end
43
129
 
44
130
  def available_locales
45
- @available_locales ||= locale_files.map { |path| File.basename(path, ".yml") }.uniq
131
+ @@available_locales ||= Set.new(locale_files.map { |path| File.basename(path, ".yml") })
46
132
  end
47
133
 
48
134
  def find_locale_files(lang)
49
135
  locale_files.select { |file| file =~ /\/#{lang}\.yml$/ }
50
136
  end
51
137
 
52
- # This is a hook for a Sidekiq Pro feature. Please don't touch.
53
- def filtering(*)
138
+ def language_name(locale)
139
+ strings(locale).fetch("LanguageName", locale)
140
+ end
141
+
142
+ def search(jobset, substr)
143
+ resultset = jobset.scan(substr).to_a
144
+ @current_page = 1
145
+ @count = @total_size = resultset.size
146
+ resultset
147
+ end
148
+
149
+ def filtering(which, placeholder_key: "AnyJobContent", label_key: "Filter")
150
+ erb(:filtering, locals: {which:, placeholder_key:, label_key:})
151
+ end
152
+
153
+ def filter_link(str, within = "retries")
154
+ hstr = h(str)
155
+ if within.nil?
156
+ hstr
157
+ else
158
+ "<a href='#{root_path}#{within}?substr=#{hstr}'>#{hstr}</a>"
159
+ end
160
+ end
161
+
162
+ def display_tags(job, within = "retries")
163
+ job.tags.map { |tag|
164
+ "<span class='label label-info jobtag jobtag-#{h(tag)}'>#{filter_link(tag, within)}</span>"
165
+ }.join(" ")
54
166
  end
55
167
 
56
168
  # This view helper provide ability display you html code in
@@ -78,10 +190,13 @@ module Sidekiq
78
190
  text_direction == "rtl"
79
191
  end
80
192
 
81
- # See https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.4
193
+ # See https://www.rfc-editor.org/rfc/rfc9110.html#section-12.5.4
194
+ # Returns an array of language tags ordered by their quality value
195
+ #
196
+ # Inspiration taken from https://github.com/iain/http_accept_language/blob/master/lib/http_accept_language/parser.rb
82
197
  def user_preferred_languages
83
198
  languages = env["HTTP_ACCEPT_LANGUAGE"]
84
- languages.to_s.downcase.gsub(/\s+/, "").split(",").map { |language|
199
+ languages.to_s.gsub(/\s+/, "").split(",").map { |language|
85
200
  locale, quality = language.split(";q=", 2)
86
201
  locale = nil if locale == "*" # Ignore wildcards
87
202
  quality = quality ? quality.to_f : 1.0
@@ -93,34 +208,39 @@ module Sidekiq
93
208
 
94
209
  # 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"
95
210
  # this method will try to best match the available locales to the user's preferred languages.
96
- #
97
- # Inspiration taken from https://github.com/iain/http_accept_language/blob/master/lib/http_accept_language/parser.rb
98
211
  def locale
99
- @locale ||= begin
100
- matched_locale = user_preferred_languages.map { |preferred|
101
- preferred_language = preferred.split("-", 2).first
212
+ # session[:locale] is set via the locale selector from the footer
213
+ @locale ||= if (l = session&.fetch(:locale, nil)) && available_locales.include?(l)
214
+ l
215
+ else
216
+ matched_locale = nil
217
+ # Attempt to find a case-insensitive exact match first
218
+ user_preferred_languages.each do |preferred|
219
+ # We only care about the language and primary subtag
220
+ # "en-GB-oxendict" becomes "en-GB"
221
+ language_tag = preferred.split("-")[0..1].join("-")
222
+ matched_locale = available_locales.find { |available_locale| available_locale.casecmp?(language_tag) }
223
+ break if matched_locale
224
+ end
102
225
 
103
- lang_group = available_locales.select { |available|
104
- preferred_language == available.split("-", 2).first
105
- }
226
+ return matched_locale if matched_locale
106
227
 
107
- lang_group.find { |lang| lang == preferred } || lang_group.min_by(&:length)
108
- }.compact.first
228
+ # Find the first base language match
229
+ # "en-US,es-MX;q=0.9" matches "en"
230
+ user_preferred_languages.each do |preferred|
231
+ base_language = preferred.split("-", 2).first
232
+ matched_locale = available_locales.find { |available_locale| available_locale.casecmp?(base_language) }
233
+ break if matched_locale
234
+ end
109
235
 
110
236
  matched_locale || "en"
111
237
  end
112
238
  end
113
239
 
114
- # within is used by Sidekiq Pro
115
- def display_tags(job, within = nil)
116
- job.tags.map { |tag|
117
- "<span class='label label-info jobtag'>#{::Rack::Utils.escape_html(tag)}</span>"
118
- }.join(" ")
119
- end
120
-
121
- # mperham/sidekiq#3243
240
+ # sidekiq/sidekiq#3243
122
241
  def unfiltered?
123
- yield unless env["PATH_INFO"].start_with?("/filter/")
242
+ s = url_params("substr")
243
+ yield unless s && s.size > 0
124
244
  end
125
245
 
126
246
  def get_locale
@@ -137,7 +257,7 @@ module Sidekiq
137
257
  end
138
258
 
139
259
  def sort_direction_label
140
- (params[:direction] == "asc") ? "&uarr;" : "&darr;"
260
+ (url_params("direction") == "asc") ? "&uarr;" : "&darr;"
141
261
  end
142
262
 
143
263
  def workset
@@ -165,18 +285,14 @@ module Sidekiq
165
285
  @stats ||= Sidekiq::Stats.new
166
286
  end
167
287
 
168
- def redis_connection
288
+ def redis_url
169
289
  Sidekiq.redis do |conn|
170
- conn.connection[:id]
290
+ conn.config.server_url
171
291
  end
172
292
  end
173
293
 
174
- def namespace
175
- @ns ||= Sidekiq.redis { |conn| conn.respond_to?(:namespace) ? conn.namespace : nil }
176
- end
177
-
178
294
  def redis_info
179
- Sidekiq.redis_info
295
+ @info ||= Sidekiq.default_configuration.redis_info
180
296
  end
181
297
 
182
298
  def root_path
@@ -196,26 +312,37 @@ module Sidekiq
196
312
  %(<time class="ltr" dir="ltr" title="#{stamp}" datetime="#{stamp}">#{time}</time>)
197
313
  end
198
314
 
315
+ def queue_names_by_capsule(pro)
316
+ cap = pro.capsules
317
+ if cap
318
+ cap.map { |k, v| v["weights"].keys.join(", ") }.join("; ")
319
+ else
320
+ # DEPRECATED Backwards compatibility with older processes.
321
+ # 'capsules' element added in v8.0.9
322
+ pro.queues.join(", ")
323
+ end
324
+ end
325
+
199
326
  def job_params(job, score)
200
327
  "#{score}-#{job["jid"]}"
201
328
  end
202
329
 
203
- def parse_params(params)
204
- score, jid = params.split("-", 2)
330
+ def parse_key(key)
331
+ score, jid = key.split("-", 2)
205
332
  [score.to_f, jid]
206
333
  end
207
334
 
208
- SAFE_QPARAMS = %w[page direction]
335
+ SAFE_QPARAMS = %w[page direction only]
209
336
 
210
337
  # Merge options with current params, filter safe params, and stringify to query string
211
338
  def qparams(options)
212
339
  stringified_options = options.transform_keys(&:to_s)
213
340
 
214
- to_query_string(params.merge(stringified_options))
341
+ to_query_string(request.params.merge(stringified_options))
215
342
  end
216
343
 
217
- def to_query_string(params)
218
- params.map { |key, value|
344
+ def to_query_string(hash)
345
+ hash.map { |key, value|
219
346
  SAFE_QPARAMS.include?(key) ? "#{key}=#{CGI.escape(value.to_s)}" : next
220
347
  }.compact.join("&")
221
348
  end
@@ -238,7 +365,11 @@ module Sidekiq
238
365
  end
239
366
 
240
367
  def csrf_tag
241
- "<input type='hidden' name='authenticity_token' value='#{env[:csrf_token]}'/>"
368
+ ""
369
+ end
370
+
371
+ def csp_nonce
372
+ env[:csp_nonce]
242
373
  end
243
374
 
244
375
  def to_display(arg)
@@ -274,27 +405,17 @@ module Sidekiq
274
405
  elsif rss_kb < 10_000_000
275
406
  "#{number_with_delimiter((rss_kb / 1024.0).to_i)} MB"
276
407
  else
277
- "#{number_with_delimiter((rss_kb / (1024.0 * 1024.0)).round(1))} GB"
408
+ "#{number_with_delimiter(rss_kb / (1024.0 * 1024.0), precision: 1)} GB"
278
409
  end
279
410
  end
280
411
 
281
- def number_with_delimiter(number)
282
- return "" if number.nil?
283
-
284
- begin
285
- Float(number)
286
- rescue ArgumentError, TypeError
287
- return number
288
- end
289
-
290
- options = {delimiter: ",", separator: "."}
291
- parts = number.to_s.to_str.split(".")
292
- parts[0].gsub!(/(\d)(?=(\d\d\d)+(?!\d))/, "\\1#{options[:delimiter]}")
293
- parts.join(options[:separator])
412
+ def number_with_delimiter(number, options = {})
413
+ precision = options[:precision] || 0
414
+ %(<span data-nwp="#{precision}">#{number.round(precision)}</span>)
294
415
  end
295
416
 
296
417
  def h(text)
297
- ::Rack::Utils.escape_html(text)
418
+ ::CGI.escapeHTML(text.to_s)
298
419
  rescue ArgumentError => e
299
420
  raise unless e.message.eql?("invalid byte sequence in UTF-8")
300
421
  text.encode!("UTF-16", "UTF-8", invalid: :replace, replace: "").encode!("UTF-8", "UTF-16")
@@ -314,7 +435,7 @@ module Sidekiq
314
435
  end
315
436
 
316
437
  def environment_title_prefix
317
- environment = Sidekiq[:environment] || ENV["APP_ENV"] || ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "development"
438
+ environment = Sidekiq.default_configuration[:environment] || ENV["APP_ENV"] || ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "development"
318
439
 
319
440
  "[#{environment.upcase}] " unless environment == "production"
320
441
  end
@@ -327,11 +448,9 @@ module Sidekiq
327
448
  Time.now.utc.strftime("%H:%M:%S UTC")
328
449
  end
329
450
 
330
- def redis_connection_and_namespace
331
- @redis_connection_and_namespace ||= begin
332
- namespace_suffix = namespace.nil? ? "" : "##{namespace}"
333
- "#{redis_connection}#{namespace_suffix}"
334
- end
451
+ def pollable?
452
+ # there's no point to refreshing the metrics pages every N seconds
453
+ !(current_path == "" || current_path.index("metrics"))
335
454
  end
336
455
 
337
456
  def retry_or_delete_or_kill(job, params)
@@ -1,103 +1,90 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "rack"
3
+ require "uri"
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(method, path, &block)
43
- @routes ||= {GET => [], POST => [], PUT => [], PATCH => [], DELETE => [], HEAD => []}
44
-
45
- @routes[method] << WebRoute.new(method, path, block)
46
- end
47
-
48
- def match(env)
49
- request_method = env[REQUEST_METHOD]
50
- path_info = ::Rack::Utils.unescape env[PATH_INFO]
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
27
+ end
51
28
 
52
- # There are servers which send an empty string when requesting the root.
53
- # These servers should be ashamed of themselves.
54
- path_info = "/" if path_info == ""
29
+ def match(env)
30
+ request_method = env["REQUEST_METHOD"].downcase.to_sym
31
+ path_info = ::URI::RFC2396_PARSER.unescape env["PATH_INFO"]
55
32
 
56
- @routes[request_method].each do |route|
57
- params = route.match(request_method, path_info)
58
- if params
59
- env[ROUTE_PARAMS] = params
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 == ""
60
36
 
61
- 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
62
43
  end
44
+
45
+ nil
63
46
  end
64
47
 
65
- nil
48
+ def route_cache
49
+ @@routes ||= {get: [], post: [], put: [], patch: [], delete: [], head: []}
50
+ end
66
51
  end
67
- end
68
52
 
69
- class WebRoute
70
- attr_accessor :request_method, :pattern, :block, :name
53
+ class Route
54
+ attr_accessor :request_method, :pattern, :block, :name
71
55
 
72
- NAMED_SEGMENTS_PATTERN = /\/([^\/]*):([^.:$\/]+)/
56
+ NAMED_SEGMENTS_PATTERN = /\/([^\/]*):([^.:$\/]+)/
73
57
 
74
- def initialize(request_method, pattern, block)
75
- @request_method = request_method
76
- @pattern = pattern
77
- @block = block
78
- end
58
+ def initialize(request_method, pattern, block)
59
+ @request_method = request_method
60
+ @pattern = pattern
61
+ @block = block
62
+ end
79
63
 
80
- def matcher
81
- @matcher ||= compile
82
- end
64
+ def matcher
65
+ @matcher ||= compile
66
+ end
83
67
 
84
- def compile
85
- if pattern.match?(NAMED_SEGMENTS_PATTERN)
86
- 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>[^$/]+)')
87
71
 
88
- Regexp.new("\\A#{p}\\Z")
89
- else
90
- pattern
72
+ Regexp.new("\\A#{p}\\Z")
73
+ else
74
+ pattern
75
+ end
91
76
  end
92
- end
93
77
 
94
- def match(request_method, path)
95
- case matcher
96
- when String
97
- {} if path == matcher
98
- else
99
- path_match = path.match(matcher)
100
- 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
101
88
  end
102
89
  end
103
90
  end