sidekiq 7.3.9 → 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 (155) hide show
  1. checksums.yaml +4 -4
  2. data/Changes.md +225 -0
  3. data/README.md +34 -14
  4. data/bin/kiq +17 -0
  5. data/bin/lint-herb +13 -0
  6. data/bin/sidekiqload +12 -10
  7. data/bin/webload +69 -0
  8. data/lib/active_job/queue_adapters/sidekiq_adapter.rb +107 -58
  9. data/lib/generators/sidekiq/job_generator.rb +15 -3
  10. data/lib/generators/sidekiq/templates/job.rb.erb +1 -1
  11. data/lib/sidekiq/api.rb +310 -83
  12. data/lib/sidekiq/capsule.rb +10 -7
  13. data/lib/sidekiq/cli.rb +31 -24
  14. data/lib/sidekiq/client.rb +36 -18
  15. data/lib/sidekiq/component.rb +45 -3
  16. data/lib/sidekiq/config.rb +35 -30
  17. data/lib/sidekiq/embedded.rb +2 -1
  18. data/lib/sidekiq/fetch.rb +1 -0
  19. data/lib/sidekiq/iterable_job.rb +1 -0
  20. data/lib/sidekiq/job/iterable.rb +46 -18
  21. data/lib/sidekiq/job.rb +18 -3
  22. data/lib/sidekiq/job_logger.rb +8 -6
  23. data/lib/sidekiq/job_retry.rb +45 -18
  24. data/lib/sidekiq/job_util.rb +5 -1
  25. data/lib/sidekiq/launcher.rb +22 -12
  26. data/lib/sidekiq/loader.rb +57 -0
  27. data/lib/sidekiq/logger.rb +25 -69
  28. data/lib/sidekiq/manager.rb +2 -2
  29. data/lib/sidekiq/metrics/query.rb +77 -46
  30. data/lib/sidekiq/metrics/shared.rb +8 -5
  31. data/lib/sidekiq/metrics/tracking.rb +12 -7
  32. data/lib/sidekiq/middleware/current_attributes.rb +11 -19
  33. data/lib/sidekiq/middleware/i18n.rb +2 -0
  34. data/lib/sidekiq/monitor.rb +4 -8
  35. data/lib/sidekiq/paginator.rb +18 -2
  36. data/lib/sidekiq/processor.rb +26 -18
  37. data/lib/sidekiq/profiler.rb +73 -0
  38. data/lib/sidekiq/rails.rb +46 -67
  39. data/lib/sidekiq/redis_client_adapter.rb +6 -3
  40. data/lib/sidekiq/redis_connection.rb +16 -5
  41. data/lib/sidekiq/ring_buffer.rb +1 -0
  42. data/lib/sidekiq/scheduled.rb +6 -7
  43. data/lib/sidekiq/test_api.rb +331 -0
  44. data/lib/sidekiq/testing/inline.rb +2 -30
  45. data/lib/sidekiq/testing.rb +2 -333
  46. data/lib/sidekiq/transaction_aware_client.rb +13 -5
  47. data/lib/sidekiq/tui/controls.rb +53 -0
  48. data/lib/sidekiq/tui/filtering.rb +53 -0
  49. data/lib/sidekiq/tui/tabs/base_tab.rb +230 -0
  50. data/lib/sidekiq/tui/tabs/busy.rb +104 -0
  51. data/lib/sidekiq/tui/tabs/dead.rb +14 -0
  52. data/lib/sidekiq/tui/tabs/home.rb +115 -0
  53. data/lib/sidekiq/tui/tabs/metrics.rb +114 -0
  54. data/lib/sidekiq/tui/tabs/queues.rb +83 -0
  55. data/lib/sidekiq/tui/tabs/retries.rb +14 -0
  56. data/lib/sidekiq/tui/tabs/scheduled.rb +14 -0
  57. data/lib/sidekiq/tui/tabs/set_tab.rb +91 -0
  58. data/lib/sidekiq/tui/tabs.rb +15 -0
  59. data/lib/sidekiq/tui.rb +390 -0
  60. data/lib/sidekiq/version.rb +2 -2
  61. data/lib/sidekiq/web/action.rb +146 -83
  62. data/lib/sidekiq/web/application.rb +372 -328
  63. data/lib/sidekiq/web/config.rb +117 -0
  64. data/lib/sidekiq/web/helpers.rb +102 -44
  65. data/lib/sidekiq/web/router.rb +61 -77
  66. data/lib/sidekiq/web.rb +65 -151
  67. data/lib/sidekiq.rb +13 -1
  68. data/sidekiq.gemspec +9 -8
  69. data/web/assets/images/logo.png +0 -0
  70. data/web/assets/images/status.png +0 -0
  71. data/web/assets/javascripts/application.js +43 -27
  72. data/web/assets/javascripts/base-charts.js +30 -16
  73. data/web/assets/javascripts/chartjs-adapter-date-fns.min.js +7 -0
  74. data/web/assets/javascripts/dashboard.js +1 -1
  75. data/web/assets/javascripts/metrics.js +16 -34
  76. data/web/assets/stylesheets/style.css +776 -0
  77. data/web/locales/ar.yml +3 -1
  78. data/web/locales/cs.yml +2 -0
  79. data/web/locales/da.yml +2 -0
  80. data/web/locales/de.yml +2 -0
  81. data/web/locales/el.yml +2 -0
  82. data/web/locales/en.yml +8 -0
  83. data/web/locales/es.yml +25 -2
  84. data/web/locales/fa.yml +3 -1
  85. data/web/locales/fr.yml +3 -1
  86. data/web/locales/gd.yml +13 -1
  87. data/web/locales/he.yml +3 -1
  88. data/web/locales/hi.yml +36 -0
  89. data/web/locales/it.yml +9 -0
  90. data/web/locales/ja.yml +2 -0
  91. data/web/locales/ko.yml +2 -0
  92. data/web/locales/lt.yml +2 -0
  93. data/web/locales/nb.yml +2 -0
  94. data/web/locales/nl.yml +2 -0
  95. data/web/locales/pl.yml +2 -0
  96. data/web/locales/{pt-br.yml → pt-BR.yml} +3 -1
  97. data/web/locales/pt.yml +2 -0
  98. data/web/locales/ru.yml +2 -0
  99. data/web/locales/sv.yml +2 -0
  100. data/web/locales/ta.yml +83 -47
  101. data/web/locales/tr.yml +2 -0
  102. data/web/locales/uk.yml +7 -5
  103. data/web/locales/ur.yml +3 -1
  104. data/web/locales/vi.yml +2 -0
  105. data/web/locales/{zh-cn.yml → zh-CN.yml} +86 -73
  106. data/web/locales/{zh-tw.yml → zh-TW.yml} +3 -1
  107. data/web/views/_footer.html.erb +32 -0
  108. data/web/views/_job_info.html.erb +115 -0
  109. data/web/views/_metrics_period_select.html.erb +15 -0
  110. data/web/views/_nav.html.erb +45 -0
  111. data/web/views/_paging.html.erb +26 -0
  112. data/web/views/_poll_link.html.erb +4 -0
  113. data/web/views/{_summary.erb → _summary.html.erb} +16 -16
  114. data/web/views/busy.html.erb +155 -0
  115. data/web/views/dashboard.html.erb +104 -0
  116. data/web/views/dead.html.erb +38 -0
  117. data/web/views/filtering.html.erb +6 -0
  118. data/web/views/layout.html.erb +26 -0
  119. data/web/views/metrics.html.erb +85 -0
  120. data/web/views/metrics_for_job.html.erb +58 -0
  121. data/web/views/morgue.html.erb +69 -0
  122. data/web/views/profiles.html.erb +43 -0
  123. data/web/views/queue.html.erb +57 -0
  124. data/web/views/queues.html.erb +46 -0
  125. data/web/views/retries.html.erb +77 -0
  126. data/web/views/retry.html.erb +39 -0
  127. data/web/views/scheduled.html.erb +64 -0
  128. data/web/views/{scheduled_job_info.erb → scheduled_job_info.html.erb} +3 -3
  129. metadata +65 -47
  130. data/lib/sidekiq/web/csrf_protection.rb +0 -183
  131. data/web/assets/stylesheets/application-dark.css +0 -147
  132. data/web/assets/stylesheets/application-rtl.css +0 -163
  133. data/web/assets/stylesheets/application.css +0 -759
  134. data/web/assets/stylesheets/bootstrap-rtl.min.css +0 -9
  135. data/web/assets/stylesheets/bootstrap.css +0 -5
  136. data/web/views/_footer.erb +0 -34
  137. data/web/views/_job_info.erb +0 -105
  138. data/web/views/_metrics_period_select.erb +0 -12
  139. data/web/views/_nav.erb +0 -52
  140. data/web/views/_paging.erb +0 -25
  141. data/web/views/_poll_link.erb +0 -4
  142. data/web/views/_status.erb +0 -4
  143. data/web/views/busy.erb +0 -148
  144. data/web/views/dashboard.erb +0 -108
  145. data/web/views/dead.erb +0 -34
  146. data/web/views/filtering.erb +0 -6
  147. data/web/views/layout.erb +0 -42
  148. data/web/views/metrics.erb +0 -90
  149. data/web/views/metrics_for_job.erb +0 -59
  150. data/web/views/morgue.erb +0 -74
  151. data/web/views/queue.erb +0 -55
  152. data/web/views/queues.erb +0 -44
  153. data/web/views/retries.erb +0 -79
  154. data/web/views/retry.erb +0 -34
  155. data/web/views/scheduled.erb +0 -56
@@ -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,14 +1,26 @@
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
7
  # These methods are available to pages within the Web UI and UI extensions.
10
8
  # They are not public APIs for applications to use.
11
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
+
12
24
  def style_tag(location, **kwargs)
13
25
  global = location.match?(/:\/\//)
14
26
  location = root_path + location if !global && !location.start_with?(root_path)
@@ -19,7 +31,9 @@ module Sidekiq
19
31
  nonce: csp_nonce,
20
32
  href: location
21
33
  }
22
- html_tag(:link, attrs.merge(kwargs))
34
+ add_to_head do
35
+ html_tag(:link, attrs.merge(kwargs))
36
+ end
23
37
  end
24
38
 
25
39
  def script_tag(location, **kwargs)
@@ -36,7 +50,7 @@ module Sidekiq
36
50
  # NB: keys and values are not escaped; do not allow user input
37
51
  # in the attributes
38
52
  private def html_tag(tagname, attrs)
39
- s = +"<#{tagname}"
53
+ s = "<#{tagname}"
40
54
  attrs.each_pair do |k, v|
41
55
  next unless v
42
56
  s << " #{k}=\"#{v}\""
@@ -56,14 +70,39 @@ module Sidekiq
56
70
 
57
71
  # Allow sidekiq-web extensions to add locale paths
58
72
  # so extensions can be localized
59
- @@strings[lang] ||= settings.locales.each_with_object({}) do |path, global|
73
+ @@strings[lang] ||= config.locales.each_with_object({}) do |path, global|
60
74
  find_locale_files(lang).each do |file|
61
- strs = YAML.safe_load(File.read(file))
75
+ strs = parse_yaml(file)
62
76
  global.merge!(strs[lang])
63
77
  end
64
78
  end
65
79
  end
66
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
+
67
106
  def to_json(x)
68
107
  Sidekiq.dump_json(x)
69
108
  end
@@ -83,7 +122,7 @@ module Sidekiq
83
122
  end
84
123
 
85
124
  def locale_files
86
- @@locale_files ||= settings.locales.flat_map { |path|
125
+ @@locale_files ||= config.locales.flat_map { |path|
87
126
  Dir["#{path}/*.yml"]
88
127
  }
89
128
  end
@@ -96,6 +135,10 @@ module Sidekiq
96
135
  locale_files.select { |file| file =~ /\/#{lang}\.yml$/ }
97
136
  end
98
137
 
138
+ def language_name(locale)
139
+ strings(locale).fetch("LanguageName", locale)
140
+ end
141
+
99
142
  def search(jobset, substr)
100
143
  resultset = jobset.scan(substr).to_a
101
144
  @current_page = 1
@@ -103,21 +146,22 @@ module Sidekiq
103
146
  resultset
104
147
  end
105
148
 
106
- def filtering(which)
107
- erb(:filtering, locals: {which: which})
149
+ def filtering(which, placeholder_key: "AnyJobContent", label_key: "Filter")
150
+ erb(:filtering, locals: {which:, placeholder_key:, label_key:})
108
151
  end
109
152
 
110
- def filter_link(jid, within = "retries")
153
+ def filter_link(str, within = "retries")
154
+ hstr = h(str)
111
155
  if within.nil?
112
- ::Rack::Utils.escape_html(jid)
156
+ hstr
113
157
  else
114
- "<a href='#{root_path}#{within}?substr=#{jid}'>#{::Rack::Utils.escape_html(jid)}</a>"
158
+ "<a href='#{root_path}#{within}?substr=#{hstr}'>#{hstr}</a>"
115
159
  end
116
160
  end
117
161
 
118
162
  def display_tags(job, within = "retries")
119
163
  job.tags.map { |tag|
120
- "<span class='label label-info jobtag'>#{filter_link(tag, within)}</span>"
164
+ "<span class='label label-info jobtag jobtag-#{h(tag)}'>#{filter_link(tag, within)}</span>"
121
165
  }.join(" ")
122
166
  end
123
167
 
@@ -146,10 +190,13 @@ module Sidekiq
146
190
  text_direction == "rtl"
147
191
  end
148
192
 
149
- # 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
150
197
  def user_preferred_languages
151
198
  languages = env["HTTP_ACCEPT_LANGUAGE"]
152
- languages.to_s.downcase.gsub(/\s+/, "").split(",").map { |language|
199
+ languages.to_s.gsub(/\s+/, "").split(",").map { |language|
153
200
  locale, quality = language.split(";q=", 2)
154
201
  locale = nil if locale == "*" # Ignore wildcards
155
202
  quality = quality ? quality.to_f : 1.0
@@ -161,22 +208,30 @@ module Sidekiq
161
208
 
162
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"
163
210
  # this method will try to best match the available locales to the user's preferred languages.
164
- #
165
- # Inspiration taken from https://github.com/iain/http_accept_language/blob/master/lib/http_accept_language/parser.rb
166
211
  def locale
167
212
  # session[:locale] is set via the locale selector from the footer
168
213
  @locale ||= if (l = session&.fetch(:locale, nil)) && available_locales.include?(l)
169
214
  l
170
215
  else
171
- matched_locale = user_preferred_languages.map { |preferred|
172
- preferred_language = preferred.split("-", 2).first
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
173
225
 
174
- lang_group = available_locales.select { |available|
175
- preferred_language == available.split("-", 2).first
176
- }
226
+ return matched_locale if matched_locale
177
227
 
178
- lang_group.find { |lang| lang == preferred } || lang_group.min_by(&:length)
179
- }.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
180
235
 
181
236
  matched_locale || "en"
182
237
  end
@@ -202,7 +257,7 @@ module Sidekiq
202
257
  end
203
258
 
204
259
  def sort_direction_label
205
- (params[:direction] == "asc") ? "&uarr;" : "&darr;"
260
+ (url_params("direction") == "asc") ? "&uarr;" : "&darr;"
206
261
  end
207
262
 
208
263
  def workset
@@ -226,14 +281,6 @@ module Sidekiq
226
281
  end
227
282
  end
228
283
 
229
- def busy_weights(capsule_weights)
230
- # backwards compat with 7.0.0, remove in 7.1
231
- cw = [capsule_weights].flatten
232
- cw.map { |hash|
233
- hash.map { |name, weight| (weight > 0) ? +name << ": " << weight.to_s : name }.join(", ")
234
- }.join("; ")
235
- end
236
-
237
284
  def stats
238
285
  @stats ||= Sidekiq::Stats.new
239
286
  end
@@ -245,7 +292,7 @@ module Sidekiq
245
292
  end
246
293
 
247
294
  def redis_info
248
- Sidekiq.default_configuration.redis_info
295
+ @info ||= Sidekiq.default_configuration.redis_info
249
296
  end
250
297
 
251
298
  def root_path
@@ -265,26 +312,37 @@ module Sidekiq
265
312
  %(<time class="ltr" dir="ltr" title="#{stamp}" datetime="#{stamp}">#{time}</time>)
266
313
  end
267
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
+
268
326
  def job_params(job, score)
269
327
  "#{score}-#{job["jid"]}"
270
328
  end
271
329
 
272
- def parse_params(params)
273
- score, jid = params.split("-", 2)
330
+ def parse_key(key)
331
+ score, jid = key.split("-", 2)
274
332
  [score.to_f, jid]
275
333
  end
276
334
 
277
- SAFE_QPARAMS = %w[page direction]
335
+ SAFE_QPARAMS = %w[page direction only]
278
336
 
279
337
  # Merge options with current params, filter safe params, and stringify to query string
280
338
  def qparams(options)
281
339
  stringified_options = options.transform_keys(&:to_s)
282
340
 
283
- to_query_string(params.merge(stringified_options))
341
+ to_query_string(request.params.merge(stringified_options))
284
342
  end
285
343
 
286
- def to_query_string(params)
287
- params.map { |key, value|
344
+ def to_query_string(hash)
345
+ hash.map { |key, value|
288
346
  SAFE_QPARAMS.include?(key) ? "#{key}=#{CGI.escape(value.to_s)}" : next
289
347
  }.compact.join("&")
290
348
  end
@@ -307,7 +365,7 @@ module Sidekiq
307
365
  end
308
366
 
309
367
  def csrf_tag
310
- "<input type='hidden' name='authenticity_token' value='#{env[:csrf_token]}'/>"
368
+ ""
311
369
  end
312
370
 
313
371
  def csp_nonce
@@ -347,7 +405,7 @@ module Sidekiq
347
405
  elsif rss_kb < 10_000_000
348
406
  "#{number_with_delimiter((rss_kb / 1024.0).to_i)} MB"
349
407
  else
350
- "#{number_with_delimiter((rss_kb / (1024.0 * 1024.0)), precision: 1)} GB"
408
+ "#{number_with_delimiter(rss_kb / (1024.0 * 1024.0), precision: 1)} GB"
351
409
  end
352
410
  end
353
411
 
@@ -357,7 +415,7 @@ module Sidekiq
357
415
  end
358
416
 
359
417
  def h(text)
360
- ::Rack::Utils.escape_html(text.to_s)
418
+ ::CGI.escapeHTML(text.to_s)
361
419
  rescue ArgumentError => e
362
420
  raise unless e.message.eql?("invalid byte sequence in UTF-8")
363
421
  text.encode!("UTF-16", "UTF-8", invalid: :replace, replace: "").encode!("UTF-8", "UTF-16")
@@ -1,106 +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(*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 = ::URI::RFC2396_PARSER.unescape 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