sidekiq 7.1.4 → 8.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (128) hide show
  1. checksums.yaml +4 -4
  2. data/Changes.md +333 -0
  3. data/README.md +16 -13
  4. data/bin/multi_queue_bench +271 -0
  5. data/bin/sidekiqload +31 -22
  6. data/bin/webload +69 -0
  7. data/lib/active_job/queue_adapters/sidekiq_adapter.rb +121 -0
  8. data/lib/generators/sidekiq/job_generator.rb +2 -0
  9. data/lib/generators/sidekiq/templates/job.rb.erb +1 -1
  10. data/lib/sidekiq/api.rb +260 -67
  11. data/lib/sidekiq/capsule.rb +17 -8
  12. data/lib/sidekiq/cli.rb +19 -20
  13. data/lib/sidekiq/client.rb +48 -15
  14. data/lib/sidekiq/component.rb +64 -3
  15. data/lib/sidekiq/config.rb +60 -18
  16. data/lib/sidekiq/deploy.rb +4 -2
  17. data/lib/sidekiq/embedded.rb +4 -1
  18. data/lib/sidekiq/fetch.rb +2 -1
  19. data/lib/sidekiq/iterable_job.rb +56 -0
  20. data/lib/sidekiq/job/interrupt_handler.rb +24 -0
  21. data/lib/sidekiq/job/iterable/active_record_enumerator.rb +53 -0
  22. data/lib/sidekiq/job/iterable/csv_enumerator.rb +47 -0
  23. data/lib/sidekiq/job/iterable/enumerators.rb +135 -0
  24. data/lib/sidekiq/job/iterable.rb +322 -0
  25. data/lib/sidekiq/job.rb +16 -5
  26. data/lib/sidekiq/job_logger.rb +15 -12
  27. data/lib/sidekiq/job_retry.rb +41 -13
  28. data/lib/sidekiq/job_util.rb +7 -1
  29. data/lib/sidekiq/launcher.rb +23 -11
  30. data/lib/sidekiq/loader.rb +57 -0
  31. data/lib/sidekiq/logger.rb +25 -69
  32. data/lib/sidekiq/manager.rb +0 -1
  33. data/lib/sidekiq/metrics/query.rb +76 -45
  34. data/lib/sidekiq/metrics/shared.rb +23 -9
  35. data/lib/sidekiq/metrics/tracking.rb +32 -15
  36. data/lib/sidekiq/middleware/current_attributes.rb +39 -14
  37. data/lib/sidekiq/middleware/i18n.rb +2 -0
  38. data/lib/sidekiq/middleware/modules.rb +2 -0
  39. data/lib/sidekiq/monitor.rb +6 -9
  40. data/lib/sidekiq/paginator.rb +16 -3
  41. data/lib/sidekiq/processor.rb +37 -20
  42. data/lib/sidekiq/profiler.rb +73 -0
  43. data/lib/sidekiq/rails.rb +47 -57
  44. data/lib/sidekiq/redis_client_adapter.rb +25 -8
  45. data/lib/sidekiq/redis_connection.rb +49 -9
  46. data/lib/sidekiq/ring_buffer.rb +3 -0
  47. data/lib/sidekiq/scheduled.rb +2 -2
  48. data/lib/sidekiq/systemd.rb +2 -0
  49. data/lib/sidekiq/testing.rb +34 -15
  50. data/lib/sidekiq/transaction_aware_client.rb +20 -5
  51. data/lib/sidekiq/version.rb +6 -2
  52. data/lib/sidekiq/web/action.rb +149 -64
  53. data/lib/sidekiq/web/application.rb +367 -297
  54. data/lib/sidekiq/web/config.rb +120 -0
  55. data/lib/sidekiq/web/csrf_protection.rb +8 -5
  56. data/lib/sidekiq/web/helpers.rb +146 -64
  57. data/lib/sidekiq/web/router.rb +61 -74
  58. data/lib/sidekiq/web.rb +53 -106
  59. data/lib/sidekiq.rb +11 -4
  60. data/sidekiq.gemspec +6 -5
  61. data/web/assets/images/logo.png +0 -0
  62. data/web/assets/images/status.png +0 -0
  63. data/web/assets/javascripts/application.js +66 -24
  64. data/web/assets/javascripts/base-charts.js +30 -16
  65. data/web/assets/javascripts/chartjs-adapter-date-fns.min.js +7 -0
  66. data/web/assets/javascripts/dashboard-charts.js +37 -11
  67. data/web/assets/javascripts/dashboard.js +15 -11
  68. data/web/assets/javascripts/metrics.js +50 -34
  69. data/web/assets/stylesheets/style.css +776 -0
  70. data/web/locales/ar.yml +2 -0
  71. data/web/locales/cs.yml +2 -0
  72. data/web/locales/da.yml +2 -0
  73. data/web/locales/de.yml +2 -0
  74. data/web/locales/el.yml +2 -0
  75. data/web/locales/en.yml +12 -1
  76. data/web/locales/es.yml +25 -2
  77. data/web/locales/fa.yml +2 -0
  78. data/web/locales/fr.yml +2 -1
  79. data/web/locales/gd.yml +2 -1
  80. data/web/locales/he.yml +2 -0
  81. data/web/locales/hi.yml +2 -0
  82. data/web/locales/it.yml +41 -1
  83. data/web/locales/ja.yml +2 -1
  84. data/web/locales/ko.yml +2 -0
  85. data/web/locales/lt.yml +2 -0
  86. data/web/locales/nb.yml +2 -0
  87. data/web/locales/nl.yml +2 -0
  88. data/web/locales/pl.yml +2 -0
  89. data/web/locales/{pt-br.yml → pt-BR.yml} +4 -3
  90. data/web/locales/pt.yml +2 -0
  91. data/web/locales/ru.yml +2 -0
  92. data/web/locales/sv.yml +2 -0
  93. data/web/locales/ta.yml +2 -0
  94. data/web/locales/tr.yml +102 -0
  95. data/web/locales/uk.yml +29 -4
  96. data/web/locales/ur.yml +2 -0
  97. data/web/locales/vi.yml +2 -0
  98. data/web/locales/{zh-cn.yml → zh-CN.yml} +86 -74
  99. data/web/locales/{zh-tw.yml → zh-TW.yml} +3 -2
  100. data/web/views/_footer.erb +31 -22
  101. data/web/views/_job_info.erb +91 -89
  102. data/web/views/_metrics_period_select.erb +13 -10
  103. data/web/views/_nav.erb +14 -21
  104. data/web/views/_paging.erb +22 -21
  105. data/web/views/_poll_link.erb +2 -2
  106. data/web/views/_summary.erb +23 -23
  107. data/web/views/busy.erb +123 -125
  108. data/web/views/dashboard.erb +71 -82
  109. data/web/views/dead.erb +31 -27
  110. data/web/views/filtering.erb +6 -0
  111. data/web/views/layout.erb +13 -29
  112. data/web/views/metrics.erb +70 -68
  113. data/web/views/metrics_for_job.erb +30 -40
  114. data/web/views/morgue.erb +65 -70
  115. data/web/views/profiles.erb +43 -0
  116. data/web/views/queue.erb +54 -52
  117. data/web/views/queues.erb +43 -37
  118. data/web/views/retries.erb +70 -75
  119. data/web/views/retry.erb +32 -27
  120. data/web/views/scheduled.erb +63 -55
  121. data/web/views/scheduled_job_info.erb +3 -3
  122. metadata +49 -27
  123. data/web/assets/stylesheets/application-dark.css +0 -147
  124. data/web/assets/stylesheets/application-rtl.css +0 -153
  125. data/web/assets/stylesheets/application.css +0 -724
  126. data/web/assets/stylesheets/bootstrap-rtl.min.css +0 -9
  127. data/web/assets/stylesheets/bootstrap.css +0 -5
  128. data/web/views/_status.erb +0 -4
@@ -0,0 +1,120 @@
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
+ # See `examples/webui-ext` for a sample web extension.
19
+ class Config
20
+ extend Forwardable
21
+
22
+ OPTIONS = {
23
+ # By default we support direct uploads to p.f.c since the UI is a JS SPA
24
+ # and very difficult for us to vendor or provide ourselves. If you are worried
25
+ # about data security and wish to self-host, you can change these URLs.
26
+ profile_view_url: "https://profiler.firefox.com/public/%s",
27
+ profile_store_url: "https://api.profiler.firefox.com/compressed-store",
28
+ # Will be false in Sidekiq 9.0.
29
+ # CSRF is unnecessary if you are using SameSite=(Strict|Lax) cookies.
30
+ csrf: true
31
+ }
32
+
33
+ ##
34
+ # Allows users to add custom rows to all of the Job
35
+ # tables, e.g. Retries, Dead, Scheduled, with custom
36
+ # links to other systems, see _job_info.erb and test
37
+ # in web_test.rb
38
+ #
39
+ # Sidekiq::Web.configure do |cfg|
40
+ # cfg.custom_job_info_rows << JobLogLink.new
41
+ # end
42
+ #
43
+ # class JobLogLink
44
+ # def add_pair(job)
45
+ # yield "External Logs", "<a href='https://example.com/logs/#{job.jid}'>Logs for #{job.jid}</a>"
46
+ # end
47
+ # end
48
+ attr_accessor :custom_job_info_rows
49
+
50
+ attr_reader :tabs
51
+ attr_reader :locales
52
+ attr_reader :views
53
+ attr_reader :middlewares
54
+
55
+ # Adds the "Back to App" link in the header
56
+ attr_accessor :app_url
57
+
58
+ def initialize
59
+ @options = OPTIONS.dup
60
+ @locales = LOCALES
61
+ @views = VIEWS
62
+ @tabs = DEFAULT_TABS.dup
63
+ @middlewares = []
64
+ @custom_job_info_rows = []
65
+ end
66
+
67
+ def_delegators :@options, :[], :[]=, :fetch, :key?, :has_key?, :merge!, :dig
68
+
69
+ def use(*args, &block)
70
+ middlewares << [args, block]
71
+ end
72
+
73
+ # Register a class as a Sidekiq Web UI extension. The class should
74
+ # provide one or more tabs which map to an index route. Options:
75
+ #
76
+ # @param extclass [Class] Class which contains the HTTP actions, required
77
+ # @param name [String] the name of the extension, used to namespace assets
78
+ # @param tab [String | Array] labels(s) of the UI tabs
79
+ # @param index [String | Array] index route(s) for each tab
80
+ # @param root_dir [String] directory location to find assets, locales and views, typically `web/` within the gemfile
81
+ # @param asset_paths [Array] one or more directories under {root}/assets/{name} to be publicly served, e.g. ["js", "css", "img"]
82
+ # @param cache_for [Integer] amount of time to cache assets, default one day
83
+ #
84
+ # Web extensions will have a root `web/` directory with `locales/`, `assets/`
85
+ # and `views/` subdirectories.
86
+ def register_extension(extclass, name:, tab:, index:, root_dir: nil, cache_for: 86400, asset_paths: nil)
87
+ tab = Array(tab)
88
+ index = Array(index)
89
+ tab.zip(index).each do |tab, index|
90
+ tabs[tab] = index
91
+ end
92
+ if root_dir
93
+ locdir = File.join(root_dir, "locales")
94
+ locales << locdir if File.directory?(locdir)
95
+
96
+ if asset_paths && name
97
+ # if you have {root}/assets/{name}/js/scripts.js
98
+ # and {root}/assets/{name}/css/styles.css
99
+ # you would pass in:
100
+ # asset_paths: ["js", "css"]
101
+ # See script_tag and style_tag in web/helpers.rb
102
+ assdir = File.join(root_dir, "assets")
103
+ assurls = Array(asset_paths).map { |x| "/#{name}/#{x}" }
104
+ assetprops = {
105
+ urls: assurls,
106
+ root: assdir,
107
+ cascade: true
108
+ }
109
+ assetprops[:header_rules] = [[:all, {"cache-control" => "private, max-age=#{cache_for.to_i}"}]] if cache_for
110
+ middlewares << [[Rack::Static, assetprops], nil]
111
+ end
112
+ end
113
+
114
+ yield self if block_given?
115
+ extclass.registered(Web::Application)
116
+ end
117
+ alias_method :register, :register_extension
118
+ end
119
+ end
120
+ end
@@ -27,7 +27,6 @@
27
27
  # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
28
28
 
29
29
  require "securerandom"
30
- require "base64"
31
30
  require "rack/request"
32
31
 
33
32
  module Sidekiq
@@ -57,7 +56,7 @@ module Sidekiq
57
56
  end
58
57
 
59
58
  def logger(env)
60
- @logger ||= (env["rack.logger"] || ::Logger.new(env["rack.errors"]))
59
+ @logger ||= env["rack.logger"] || ::Logger.new(env["rack.errors"])
61
60
  end
62
61
 
63
62
  def deny(env)
@@ -116,7 +115,7 @@ module Sidekiq
116
115
  sess = session(env)
117
116
  localtoken = sess[:csrf]
118
117
 
119
- # Checks that Rack::Session::Cookie actualy contains the csrf toekn
118
+ # Checks that Rack::Session::Cookie actually contains the csrf token
120
119
  return false if localtoken.nil?
121
120
 
122
121
  # Rotate the session token after every use
@@ -143,7 +142,7 @@ module Sidekiq
143
142
  one_time_pad = SecureRandom.random_bytes(token.length)
144
143
  encrypted_token = xor_byte_strings(one_time_pad, token)
145
144
  masked_token = one_time_pad + encrypted_token
146
- Base64.urlsafe_encode64(masked_token)
145
+ encode_token(masked_token)
147
146
  end
148
147
 
149
148
  # Essentially the inverse of +mask_token+.
@@ -168,8 +167,12 @@ module Sidekiq
168
167
  ::Rack::Utils.secure_compare(token.to_s, decode_token(local).to_s)
169
168
  end
170
169
 
170
+ def encode_token(token)
171
+ [token].pack("m0").tr("+/", "-_")
172
+ end
173
+
171
174
  def decode_token(token)
172
- Base64.urlsafe_decode64(token)
175
+ token.tr("-_", "+/").unpack1("m0")
173
176
  end
174
177
 
175
178
  def xor_byte_strings(s1, s2)
@@ -1,26 +1,88 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "uri"
4
- require "set"
5
4
  require "yaml"
6
- require "cgi"
5
+ require "cgi/escape"
7
6
 
8
7
  module Sidekiq
9
- # This is not a public API
8
+ # These methods are available to pages within the Web UI and UI extensions.
9
+ # They are not public APIs for applications to use.
10
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
+
25
+ def style_tag(location, **kwargs)
26
+ global = location.match?(/:\/\//)
27
+ location = root_path + location if !global && !location.start_with?(root_path)
28
+ attrs = {
29
+ type: "text/css",
30
+ media: "screen",
31
+ rel: "stylesheet",
32
+ nonce: csp_nonce,
33
+ href: location
34
+ }
35
+ add_to_head do
36
+ html_tag(:link, attrs.merge(kwargs))
37
+ end
38
+ end
39
+
40
+ def script_tag(location, **kwargs)
41
+ global = location.match?(/:\/\//)
42
+ location = root_path + location if !global && !location.start_with?(root_path)
43
+ attrs = {
44
+ type: "text/javascript",
45
+ nonce: csp_nonce,
46
+ src: location
47
+ }
48
+ html_tag(:script, attrs.merge(kwargs)) {}
49
+ end
50
+
51
+ # NB: keys and values are not escaped; do not allow user input
52
+ # in the attributes
53
+ private def html_tag(tagname, attrs)
54
+ s = "<#{tagname}"
55
+ attrs.each_pair do |k, v|
56
+ next unless v
57
+ s << " #{k}=\"#{v}\""
58
+ end
59
+ if block_given?
60
+ s << ">"
61
+ yield s
62
+ s << "</#{tagname}>"
63
+ else
64
+ s << " />"
65
+ end
66
+ s
67
+ end
68
+
11
69
  def strings(lang)
12
- @strings ||= {}
70
+ @@strings ||= {}
13
71
 
14
72
  # Allow sidekiq-web extensions to add locale paths
15
73
  # so extensions can be localized
16
- @strings[lang] ||= settings.locales.each_with_object({}) do |path, global|
74
+ @@strings[lang] ||= config.locales.each_with_object({}) do |path, global|
17
75
  find_locale_files(lang).each do |file|
18
- strs = YAML.safe_load(File.read(file))
76
+ strs = YAML.safe_load_file(file)
19
77
  global.merge!(strs[lang])
20
78
  end
21
79
  end
22
80
  end
23
81
 
82
+ def to_json(x)
83
+ Sidekiq.dump_json(x)
84
+ end
85
+
24
86
  def singularize(str, count)
25
87
  if count == 1 && str.respond_to?(:singularize) # rails
26
88
  str.singularize
@@ -30,27 +92,52 @@ module Sidekiq
30
92
  end
31
93
 
32
94
  def clear_caches
33
- @strings = nil
34
- @locale_files = nil
35
- @available_locales = nil
95
+ @@strings = nil
96
+ @@locale_files = nil
97
+ @@available_locales = nil
36
98
  end
37
99
 
38
100
  def locale_files
39
- @locale_files ||= settings.locales.flat_map { |path|
101
+ @@locale_files ||= config.locales.flat_map { |path|
40
102
  Dir["#{path}/*.yml"]
41
103
  }
42
104
  end
43
105
 
44
106
  def available_locales
45
- @available_locales ||= locale_files.map { |path| File.basename(path, ".yml") }.uniq
107
+ @@available_locales ||= Set.new(locale_files.map { |path| File.basename(path, ".yml") })
46
108
  end
47
109
 
48
110
  def find_locale_files(lang)
49
111
  locale_files.select { |file| file =~ /\/#{lang}\.yml$/ }
50
112
  end
51
113
 
52
- # This is a hook for a Sidekiq Pro feature. Please don't touch.
53
- def filtering(*)
114
+ def language_name(locale)
115
+ strings(locale).fetch("LanguageName", locale)
116
+ end
117
+
118
+ def search(jobset, substr)
119
+ resultset = jobset.scan(substr).to_a
120
+ @current_page = 1
121
+ @count = @total_size = resultset.size
122
+ resultset
123
+ end
124
+
125
+ def filtering(which)
126
+ erb(:filtering, locals: {which: which})
127
+ end
128
+
129
+ def filter_link(jid, within = "retries")
130
+ if within.nil?
131
+ ::Rack::Utils.escape_html(jid)
132
+ else
133
+ "<a href='#{root_path}#{within}?substr=#{jid}'>#{::Rack::Utils.escape_html(jid)}</a>"
134
+ end
135
+ end
136
+
137
+ def display_tags(job, within = "retries")
138
+ job.tags.map { |tag|
139
+ "<span class='label label-info jobtag jobtag-#{Rack::Utils.escape_html(tag)}'>#{filter_link(tag, within)}</span>"
140
+ }.join(" ")
54
141
  end
55
142
 
56
143
  # This view helper provide ability display you html code in
@@ -78,10 +165,13 @@ module Sidekiq
78
165
  text_direction == "rtl"
79
166
  end
80
167
 
81
- # See https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.4
168
+ # See https://www.rfc-editor.org/rfc/rfc9110.html#section-12.5.4
169
+ # Returns an array of language tags ordered by their quality value
170
+ #
171
+ # Inspiration taken from https://github.com/iain/http_accept_language/blob/master/lib/http_accept_language/parser.rb
82
172
  def user_preferred_languages
83
173
  languages = env["HTTP_ACCEPT_LANGUAGE"]
84
- languages.to_s.downcase.gsub(/\s+/, "").split(",").map { |language|
174
+ languages.to_s.gsub(/\s+/, "").split(",").map { |language|
85
175
  locale, quality = language.split(";q=", 2)
86
176
  locale = nil if locale == "*" # Ignore wildcards
87
177
  quality = quality ? quality.to_f : 1.0
@@ -93,34 +183,39 @@ module Sidekiq
93
183
 
94
184
  # 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
185
  # 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
186
  def locale
99
- @locale ||= begin
100
- matched_locale = user_preferred_languages.map { |preferred|
101
- preferred_language = preferred.split("-", 2).first
187
+ # session[:locale] is set via the locale selector from the footer
188
+ @locale ||= if (l = session&.fetch(:locale, nil)) && available_locales.include?(l)
189
+ l
190
+ else
191
+ matched_locale = nil
192
+ # Attempt to find a case-insensitive exact match first
193
+ user_preferred_languages.each do |preferred|
194
+ # We only care about the language and primary subtag
195
+ # "en-GB-oxendict" becomes "en-GB"
196
+ language_tag = preferred.split("-")[0..1].join("-")
197
+ matched_locale = available_locales.find { |available_locale| available_locale.casecmp?(language_tag) }
198
+ break if matched_locale
199
+ end
102
200
 
103
- lang_group = available_locales.select { |available|
104
- preferred_language == available.split("-", 2).first
105
- }
201
+ return matched_locale if matched_locale
106
202
 
107
- lang_group.find { |lang| lang == preferred } || lang_group.min_by(&:length)
108
- }.compact.first
203
+ # Find the first base language match
204
+ # "en-US,es-MX;q=0.9" matches "en"
205
+ user_preferred_languages.each do |preferred|
206
+ base_language = preferred.split("-", 2).first
207
+ matched_locale = available_locales.find { |available_locale| available_locale.casecmp?(base_language) }
208
+ break if matched_locale
209
+ end
109
210
 
110
211
  matched_locale || "en"
111
212
  end
112
213
  end
113
214
 
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
215
  # sidekiq/sidekiq#3243
122
216
  def unfiltered?
123
- yield unless env["PATH_INFO"].start_with?("/filter/")
217
+ s = url_params("substr")
218
+ yield unless s && s.size > 0
124
219
  end
125
220
 
126
221
  def get_locale
@@ -137,7 +232,7 @@ module Sidekiq
137
232
  end
138
233
 
139
234
  def sort_direction_label
140
- (params[:direction] == "asc") ? "&uarr;" : "&darr;"
235
+ (url_params("direction") == "asc") ? "&uarr;" : "&darr;"
141
236
  end
142
237
 
143
238
  def workset
@@ -161,14 +256,6 @@ module Sidekiq
161
256
  end
162
257
  end
163
258
 
164
- def busy_weights(capsule_weights)
165
- # backwards compat with 7.0.0, remove in 7.1
166
- cw = [capsule_weights].flatten
167
- cw.map { |hash|
168
- hash.map { |name, weight| (weight > 0) ? +name << ": " << weight.to_s : name }.join(", ")
169
- }.join("; ")
170
- end
171
-
172
259
  def stats
173
260
  @stats ||= Sidekiq::Stats.new
174
261
  end
@@ -180,7 +267,7 @@ module Sidekiq
180
267
  end
181
268
 
182
269
  def redis_info
183
- Sidekiq.default_configuration.redis_info
270
+ @info ||= Sidekiq.default_configuration.redis_info
184
271
  end
185
272
 
186
273
  def root_path
@@ -204,8 +291,8 @@ module Sidekiq
204
291
  "#{score}-#{job["jid"]}"
205
292
  end
206
293
 
207
- def parse_params(params)
208
- score, jid = params.split("-", 2)
294
+ def parse_key(key)
295
+ score, jid = key.split("-", 2)
209
296
  [score.to_f, jid]
210
297
  end
211
298
 
@@ -215,11 +302,11 @@ module Sidekiq
215
302
  def qparams(options)
216
303
  stringified_options = options.transform_keys(&:to_s)
217
304
 
218
- to_query_string(params.merge(stringified_options))
305
+ to_query_string(request.params.merge(stringified_options))
219
306
  end
220
307
 
221
- def to_query_string(params)
222
- params.map { |key, value|
308
+ def to_query_string(hash)
309
+ hash.map { |key, value|
223
310
  SAFE_QPARAMS.include?(key) ? "#{key}=#{CGI.escape(value.to_s)}" : next
224
311
  }.compact.join("&")
225
312
  end
@@ -245,6 +332,10 @@ module Sidekiq
245
332
  "<input type='hidden' name='authenticity_token' value='#{env[:csrf_token]}'/>"
246
333
  end
247
334
 
335
+ def csp_nonce
336
+ env[:csp_nonce]
337
+ end
338
+
248
339
  def to_display(arg)
249
340
  arg.inspect
250
341
  rescue
@@ -278,27 +369,17 @@ module Sidekiq
278
369
  elsif rss_kb < 10_000_000
279
370
  "#{number_with_delimiter((rss_kb / 1024.0).to_i)} MB"
280
371
  else
281
- "#{number_with_delimiter((rss_kb / (1024.0 * 1024.0)).round(1))} GB"
372
+ "#{number_with_delimiter(rss_kb / (1024.0 * 1024.0), precision: 1)} GB"
282
373
  end
283
374
  end
284
375
 
285
- def number_with_delimiter(number)
286
- return "" if number.nil?
287
-
288
- begin
289
- Float(number)
290
- rescue ArgumentError, TypeError
291
- return number
292
- end
293
-
294
- options = {delimiter: ",", separator: "."}
295
- parts = number.to_s.to_str.split(".")
296
- parts[0].gsub!(/(\d)(?=(\d\d\d)+(?!\d))/, "\\1#{options[:delimiter]}")
297
- parts.join(options[:separator])
376
+ def number_with_delimiter(number, options = {})
377
+ precision = options[:precision] || 0
378
+ %(<span data-nwp="#{precision}">#{number.round(precision)}</span>)
298
379
  end
299
380
 
300
381
  def h(text)
301
- ::Rack::Utils.escape_html(text)
382
+ ::Rack::Utils.escape_html(text.to_s)
302
383
  rescue ArgumentError => e
303
384
  raise unless e.message.eql?("invalid byte sequence in UTF-8")
304
385
  text.encode!("UTF-16", "UTF-8", invalid: :replace, replace: "").encode!("UTF-8", "UTF-16")
@@ -332,7 +413,8 @@ module Sidekiq
332
413
  end
333
414
 
334
415
  def pollable?
335
- !(current_path == "" || current_path.start_with?("metrics"))
416
+ # there's no point to refreshing the metrics pages every N seconds
417
+ !(current_path == "" || current_path.index("metrics"))
336
418
  end
337
419
 
338
420
  def retry_or_delete_or_kill(job, params)
@@ -3,101 +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(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 = ::Rack::Utils.unescape_path 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