sidekiq 4.2.4 → 6.4.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of sidekiq might be problematic. Click here for more details.

Files changed (143) hide show
  1. checksums.yaml +5 -5
  2. data/Changes.md +523 -0
  3. data/LICENSE +3 -3
  4. data/README.md +23 -36
  5. data/bin/sidekiq +26 -2
  6. data/bin/sidekiqload +28 -38
  7. data/bin/sidekiqmon +8 -0
  8. data/lib/generators/sidekiq/job_generator.rb +57 -0
  9. data/lib/generators/sidekiq/templates/{worker.rb.erb → job.rb.erb} +2 -2
  10. data/lib/generators/sidekiq/templates/job_spec.rb.erb +6 -0
  11. data/lib/generators/sidekiq/templates/job_test.rb.erb +8 -0
  12. data/lib/sidekiq/api.rb +403 -243
  13. data/lib/sidekiq/cli.rb +230 -211
  14. data/lib/sidekiq/client.rb +53 -64
  15. data/lib/sidekiq/delay.rb +43 -0
  16. data/lib/sidekiq/exception_handler.rb +12 -16
  17. data/lib/sidekiq/extensions/action_mailer.rb +15 -24
  18. data/lib/sidekiq/extensions/active_record.rb +15 -12
  19. data/lib/sidekiq/extensions/class_methods.rb +16 -13
  20. data/lib/sidekiq/extensions/generic_proxy.rb +14 -6
  21. data/lib/sidekiq/fetch.rb +39 -31
  22. data/lib/sidekiq/job.rb +13 -0
  23. data/lib/sidekiq/job_logger.rb +63 -0
  24. data/lib/sidekiq/job_retry.rb +261 -0
  25. data/lib/sidekiq/job_util.rb +65 -0
  26. data/lib/sidekiq/launcher.rb +170 -71
  27. data/lib/sidekiq/logger.rb +166 -0
  28. data/lib/sidekiq/manager.rb +21 -26
  29. data/lib/sidekiq/middleware/chain.rb +20 -8
  30. data/lib/sidekiq/middleware/current_attributes.rb +57 -0
  31. data/lib/sidekiq/middleware/i18n.rb +5 -7
  32. data/lib/sidekiq/monitor.rb +133 -0
  33. data/lib/sidekiq/paginator.rb +18 -14
  34. data/lib/sidekiq/processor.rb +161 -70
  35. data/lib/sidekiq/rails.rb +41 -73
  36. data/lib/sidekiq/redis_connection.rb +65 -20
  37. data/lib/sidekiq/scheduled.rb +95 -34
  38. data/lib/sidekiq/sd_notify.rb +149 -0
  39. data/lib/sidekiq/systemd.rb +24 -0
  40. data/lib/sidekiq/testing/inline.rb +2 -1
  41. data/lib/sidekiq/testing.rb +52 -26
  42. data/lib/sidekiq/util.rb +60 -14
  43. data/lib/sidekiq/version.rb +2 -1
  44. data/lib/sidekiq/web/action.rb +15 -15
  45. data/lib/sidekiq/web/application.rb +115 -89
  46. data/lib/sidekiq/web/csrf_protection.rb +180 -0
  47. data/lib/sidekiq/web/helpers.rb +151 -83
  48. data/lib/sidekiq/web/router.rb +27 -19
  49. data/lib/sidekiq/web.rb +65 -109
  50. data/lib/sidekiq/worker.rb +284 -41
  51. data/lib/sidekiq.rb +93 -60
  52. data/sidekiq.gemspec +24 -22
  53. data/web/assets/images/apple-touch-icon.png +0 -0
  54. data/web/assets/javascripts/application.js +83 -64
  55. data/web/assets/javascripts/dashboard.js +81 -85
  56. data/web/assets/stylesheets/application-dark.css +143 -0
  57. data/web/assets/stylesheets/application-rtl.css +242 -0
  58. data/web/assets/stylesheets/application.css +319 -143
  59. data/web/assets/stylesheets/bootstrap-rtl.min.css +9 -0
  60. data/web/assets/stylesheets/bootstrap.css +2 -2
  61. data/web/locales/ar.yml +87 -0
  62. data/web/locales/de.yml +14 -2
  63. data/web/locales/en.yml +8 -1
  64. data/web/locales/es.yml +22 -5
  65. data/web/locales/fa.yml +80 -0
  66. data/web/locales/fr.yml +10 -3
  67. data/web/locales/he.yml +79 -0
  68. data/web/locales/ja.yml +12 -4
  69. data/web/locales/lt.yml +83 -0
  70. data/web/locales/pl.yml +4 -4
  71. data/web/locales/ru.yml +4 -0
  72. data/web/locales/ur.yml +80 -0
  73. data/web/locales/vi.yml +83 -0
  74. data/web/views/_footer.erb +5 -2
  75. data/web/views/_job_info.erb +4 -3
  76. data/web/views/_nav.erb +4 -18
  77. data/web/views/_paging.erb +1 -1
  78. data/web/views/_poll_link.erb +2 -5
  79. data/web/views/_summary.erb +7 -7
  80. data/web/views/busy.erb +60 -22
  81. data/web/views/dashboard.erb +23 -15
  82. data/web/views/dead.erb +3 -3
  83. data/web/views/layout.erb +14 -3
  84. data/web/views/morgue.erb +19 -12
  85. data/web/views/queue.erb +24 -14
  86. data/web/views/queues.erb +14 -4
  87. data/web/views/retries.erb +22 -13
  88. data/web/views/retry.erb +4 -4
  89. data/web/views/scheduled.erb +7 -4
  90. metadata +49 -198
  91. data/.github/contributing.md +0 -32
  92. data/.github/issue_template.md +0 -4
  93. data/.gitignore +0 -12
  94. data/.travis.yml +0 -12
  95. data/3.0-Upgrade.md +0 -70
  96. data/4.0-Upgrade.md +0 -53
  97. data/COMM-LICENSE +0 -95
  98. data/Ent-Changes.md +0 -146
  99. data/Gemfile +0 -29
  100. data/Pro-2.0-Upgrade.md +0 -138
  101. data/Pro-3.0-Upgrade.md +0 -44
  102. data/Pro-Changes.md +0 -585
  103. data/Rakefile +0 -9
  104. data/bin/sidekiqctl +0 -99
  105. data/code_of_conduct.md +0 -50
  106. data/lib/generators/sidekiq/templates/worker_spec.rb.erb +0 -6
  107. data/lib/generators/sidekiq/templates/worker_test.rb.erb +0 -8
  108. data/lib/generators/sidekiq/worker_generator.rb +0 -49
  109. data/lib/sidekiq/core_ext.rb +0 -106
  110. data/lib/sidekiq/logging.rb +0 -106
  111. data/lib/sidekiq/middleware/server/active_record.rb +0 -13
  112. data/lib/sidekiq/middleware/server/logging.rb +0 -40
  113. data/lib/sidekiq/middleware/server/retry_jobs.rb +0 -205
  114. data/test/config.yml +0 -9
  115. data/test/env_based_config.yml +0 -11
  116. data/test/fake_env.rb +0 -1
  117. data/test/fixtures/en.yml +0 -2
  118. data/test/helper.rb +0 -75
  119. data/test/test_actors.rb +0 -138
  120. data/test/test_api.rb +0 -528
  121. data/test/test_cli.rb +0 -418
  122. data/test/test_client.rb +0 -266
  123. data/test/test_exception_handler.rb +0 -56
  124. data/test/test_extensions.rb +0 -127
  125. data/test/test_fetch.rb +0 -50
  126. data/test/test_launcher.rb +0 -95
  127. data/test/test_logging.rb +0 -35
  128. data/test/test_manager.rb +0 -50
  129. data/test/test_middleware.rb +0 -158
  130. data/test/test_processor.rb +0 -235
  131. data/test/test_rails.rb +0 -22
  132. data/test/test_redis_connection.rb +0 -132
  133. data/test/test_retry.rb +0 -326
  134. data/test/test_retry_exhausted.rb +0 -149
  135. data/test/test_scheduled.rb +0 -115
  136. data/test/test_scheduling.rb +0 -58
  137. data/test/test_sidekiq.rb +0 -107
  138. data/test/test_testing.rb +0 -143
  139. data/test/test_testing_fake.rb +0 -357
  140. data/test/test_testing_inline.rb +0 -94
  141. data/test/test_util.rb +0 -13
  142. data/test/test_web.rb +0 -726
  143. data/test/test_web_helpers.rb +0 -54
@@ -1,33 +1,48 @@
1
1
  # frozen_string_literal: true
2
- require 'uri'
3
- require 'yaml'
2
+
3
+ require "uri"
4
+ require "set"
5
+ require "yaml"
6
+ require "cgi"
4
7
 
5
8
  module Sidekiq
6
9
  # This is not a public API
7
10
  module WebHelpers
8
11
  def strings(lang)
9
- @@strings ||= {}
10
- @@strings[lang] ||= begin
11
- # Allow sidekiq-web extensions to add locale paths
12
- # so extensions can be localized
13
- settings.locales.each_with_object({}) do |path, global|
14
- find_locale_files(lang).each do |file|
15
- strs = YAML.load(File.open(file))
16
- global.deep_merge!(strs[lang])
17
- end
12
+ @strings ||= {}
13
+
14
+ # Allow sidekiq-web extensions to add locale paths
15
+ # so extensions can be localized
16
+ @strings[lang] ||= settings.locales.each_with_object({}) do |path, global|
17
+ find_locale_files(lang).each do |file|
18
+ strs = YAML.load(File.open(file))
19
+ global.merge!(strs[lang])
18
20
  end
19
21
  end
20
22
  end
21
23
 
24
+ def singularize(str, count)
25
+ if count == 1 && str.respond_to?(:singularize) # rails
26
+ str.singularize
27
+ else
28
+ str
29
+ end
30
+ end
31
+
22
32
  def clear_caches
23
- @@strings = nil
24
- @@locale_files = nil
33
+ @strings = nil
34
+ @locale_files = nil
35
+ @available_locales = nil
25
36
  end
26
37
 
27
38
  def locale_files
28
- @@locale_files ||= settings.locales.flat_map do |path|
39
+ @locale_files ||= settings.locales.flat_map { |path|
29
40
  Dir["#{path}/*.yml"]
30
- end
41
+ }
42
+ end
43
+
44
+ def available_locales
45
+ @available_locales ||= locale_files.map { |path| File.basename(path, ".yml") }.uniq
31
46
  end
32
47
 
33
48
  def find_locale_files(lang)
@@ -55,37 +70,65 @@ module Sidekiq
55
70
  @head_html.join if defined?(@head_html)
56
71
  end
57
72
 
58
- def poll_path
59
- if current_path != '' && params['poll']
60
- root_path + current_path
61
- else
62
- ""
63
- end
73
+ def text_direction
74
+ get_locale["TextDirection"] || "ltr"
64
75
  end
65
76
 
66
- # Given a browser request Accept-Language header like
67
- # "fr-FR,fr;q=0.8,en-US;q=0.6,en;q=0.4,ru;q=0.2", this function
68
- # will return "fr" since that's the first code with a matching
69
- # locale in web/locales
77
+ def rtl?
78
+ text_direction == "rtl"
79
+ end
80
+
81
+ # See https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.4
82
+ def user_preferred_languages
83
+ languages = env["HTTP_ACCEPT_LANGUAGE"]
84
+ languages.to_s.downcase.gsub(/\s+/, "").split(",").map { |language|
85
+ locale, quality = language.split(";q=", 2)
86
+ locale = nil if locale == "*" # Ignore wildcards
87
+ quality = quality ? quality.to_f : 1.0
88
+ [locale, quality]
89
+ }.sort { |(_, left), (_, right)|
90
+ right <=> left
91
+ }.map(&:first).compact
92
+ end
93
+
94
+ # 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
+ # 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
70
98
  def locale
71
99
  @locale ||= begin
72
- locale = 'en'.freeze
73
- languages = env['HTTP_ACCEPT_LANGUAGE'.freeze] || 'en'.freeze
74
- languages.downcase.split(','.freeze).each do |lang|
75
- next if lang == '*'.freeze
76
- lang = lang.split(';'.freeze)[0]
77
- break locale = lang if find_locale_files(lang).any?
78
- end
79
- locale
100
+ matched_locale = user_preferred_languages.map { |preferred|
101
+ preferred_language = preferred.split("-", 2).first
102
+
103
+ lang_group = available_locales.select { |available|
104
+ preferred_language == available.split("-", 2).first
105
+ }
106
+
107
+ lang_group.find { |lang| lang == preferred } || lang_group.min_by(&:length)
108
+ }.compact.first
109
+
110
+ matched_locale || "en"
80
111
  end
81
112
  end
82
113
 
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
122
+ def unfiltered?
123
+ yield unless env["PATH_INFO"].start_with?("/filter/")
124
+ end
125
+
83
126
  def get_locale
84
127
  strings(locale)
85
128
  end
86
129
 
87
- def t(msg, options={})
88
- string = get_locale[msg] || msg
130
+ def t(msg, options = {})
131
+ string = get_locale[msg] || strings("en")[msg] || msg
89
132
  if options.empty?
90
133
  string
91
134
  else
@@ -93,6 +136,10 @@ module Sidekiq
93
136
  end
94
137
  end
95
138
 
139
+ def sort_direction_label
140
+ params[:direction] == "asc" ? "&uarr;" : "&darr;"
141
+ end
142
+
96
143
  def workers
97
144
  @workers ||= Sidekiq::Workers.new
98
145
  end
@@ -105,22 +152,14 @@ module Sidekiq
105
152
  @stats ||= Sidekiq::Stats.new
106
153
  end
107
154
 
108
- def retries_with_score(score)
109
- Sidekiq.redis do |conn|
110
- conn.zrangebyscore('retry', score, score)
111
- end.map { |msg| Sidekiq.load_json(msg) }
112
- end
113
-
114
- def location
115
- Sidekiq.redis { |conn| conn.client.location }
116
- end
117
-
118
155
  def redis_connection
119
- Sidekiq.redis { |conn| conn.client.id }
156
+ Sidekiq.redis do |conn|
157
+ conn.connection[:id]
158
+ end
120
159
  end
121
160
 
122
161
  def namespace
123
- @@ns ||= Sidekiq.redis { |conn| conn.respond_to?(:namespace) ? conn.namespace : nil }
162
+ @ns ||= Sidekiq.redis { |conn| conn.respond_to?(:namespace) ? conn.namespace : nil }
124
163
  end
125
164
 
126
165
  def redis_info
@@ -128,38 +167,44 @@ module Sidekiq
128
167
  end
129
168
 
130
169
  def root_path
131
- "#{env['SCRIPT_NAME']}/"
170
+ "#{env["SCRIPT_NAME"]}/"
132
171
  end
133
172
 
134
173
  def current_path
135
- @current_path ||= request.path_info.gsub(/^\//,'')
174
+ @current_path ||= request.path_info.gsub(/^\//, "")
136
175
  end
137
176
 
138
177
  def current_status
139
- workers.size == 0 ? 'idle' : 'active'
178
+ workers.size == 0 ? "idle" : "active"
140
179
  end
141
180
 
142
181
  def relative_time(time)
143
- %{<time datetime="#{time.getutc.iso8601}">#{time}</time>}
182
+ stamp = time.getutc.iso8601
183
+ %(<time class="ltr" dir="ltr" title="#{stamp}" datetime="#{stamp}">#{time}</time>)
144
184
  end
145
185
 
146
186
  def job_params(job, score)
147
- "#{score}-#{job['jid']}"
187
+ "#{score}-#{job["jid"]}"
148
188
  end
149
189
 
150
190
  def parse_params(params)
151
- score, jid = params.split("-")
191
+ score, jid = params.split("-", 2)
152
192
  [score.to_f, jid]
153
193
  end
154
194
 
155
- SAFE_QPARAMS = %w(page poll)
195
+ SAFE_QPARAMS = %w[page direction]
156
196
 
157
197
  # Merge options with current params, filter safe params, and stringify to query string
158
198
  def qparams(options)
159
- options = options.stringify_keys
160
- params.merge(options).map do |key, value|
161
- SAFE_QPARAMS.include?(key) ? "#{key}=#{value}" : next
162
- end.compact.join("&")
199
+ stringified_options = options.transform_keys(&:to_s)
200
+
201
+ to_query_string(params.merge(stringified_options))
202
+ end
203
+
204
+ def to_query_string(params)
205
+ params.map { |key, value|
206
+ SAFE_QPARAMS.include?(key) ? "#{key}=#{CGI.escape(value.to_s)}" : next
207
+ }.compact.join("&")
163
208
  end
164
209
 
165
210
  def truncate(text, truncate_after_chars = 2000)
@@ -167,33 +212,38 @@ module Sidekiq
167
212
  end
168
213
 
169
214
  def display_args(args, truncate_after_chars = 2000)
170
- args.map do |arg|
171
- h(truncate(to_display(arg), truncate_after_chars))
172
- end.join(", ")
215
+ return "Invalid job payload, args is nil" if args.nil?
216
+ return "Invalid job payload, args must be an Array, not #{args.class.name}" unless args.is_a?(Array)
217
+
218
+ begin
219
+ args.map { |arg|
220
+ h(truncate(to_display(arg), truncate_after_chars))
221
+ }.join(", ")
222
+ rescue
223
+ "Illegal job arguments: #{h args.inspect}"
224
+ end
173
225
  end
174
226
 
175
227
  def csrf_tag
176
- "<input type='hidden' name='authenticity_token' value='#{session[:csrf]}'/>"
228
+ "<input type='hidden' name='authenticity_token' value='#{env[:csrf_token]}'/>"
177
229
  end
178
230
 
179
231
  def to_display(arg)
232
+ arg.inspect
233
+ rescue
180
234
  begin
181
- arg.inspect
182
- rescue
183
- begin
184
- arg.to_s
185
- rescue => ex
186
- "Cannot display argument: [#{ex.class.name}] #{ex.message}"
187
- end
235
+ arg.to_s
236
+ rescue => ex
237
+ "Cannot display argument: [#{ex.class.name}] #{ex.message}"
188
238
  end
189
239
  end
190
240
 
191
- RETRY_JOB_KEYS = Set.new(%w(
241
+ RETRY_JOB_KEYS = Set.new(%w[
192
242
  queue class args retry_count retried_at failed_at
193
243
  jid error_message error_class backtrace
194
244
  error_backtrace enqueued_at retry wrapped
195
- created_at
196
- ))
245
+ created_at tags
246
+ ])
197
247
 
198
248
  def retry_extra_items(retry_job)
199
249
  @retry_extra_items ||= {}.tap do |extra|
@@ -203,15 +253,29 @@ module Sidekiq
203
253
  end
204
254
  end
205
255
 
256
+ def format_memory(rss_kb)
257
+ return "0" if rss_kb.nil? || rss_kb == 0
258
+
259
+ if rss_kb < 100_000
260
+ "#{number_with_delimiter(rss_kb)} KB"
261
+ elsif rss_kb < 10_000_000
262
+ "#{number_with_delimiter((rss_kb / 1024.0).to_i)} MB"
263
+ else
264
+ "#{number_with_delimiter((rss_kb / (1024.0 * 1024.0)).round(1))} GB"
265
+ end
266
+ end
267
+
206
268
  def number_with_delimiter(number)
269
+ return "" if number.nil?
270
+
207
271
  begin
208
272
  Float(number)
209
273
  rescue ArgumentError, TypeError
210
274
  return number
211
275
  end
212
276
 
213
- options = {delimiter: ',', separator: '.'}
214
- parts = number.to_s.to_str.split('.')
277
+ options = {delimiter: ",", separator: "."}
278
+ parts = number.to_s.to_str.split(".")
215
279
  parts[0].gsub!(/(\d)(?=(\d\d\d)+(?!\d))/, "\\1#{options[:delimiter]}")
216
280
  parts.join(options[:separator])
217
281
  end
@@ -219,8 +283,8 @@ module Sidekiq
219
283
  def h(text)
220
284
  ::Rack::Utils.escape_html(text)
221
285
  rescue ArgumentError => e
222
- raise unless e.message.eql?('invalid byte sequence in UTF-8')
223
- text.encode!('UTF-16', 'UTF-8', invalid: :replace, replace: '').encode!('UTF-8', 'UTF-16')
286
+ raise unless e.message.eql?("invalid byte sequence in UTF-8")
287
+ text.encode!("UTF-16", "UTF-8", invalid: :replace, replace: "").encode!("UTF-8", "UTF-16")
224
288
  retry
225
289
  end
226
290
 
@@ -237,7 +301,7 @@ module Sidekiq
237
301
  end
238
302
 
239
303
  def environment_title_prefix
240
- environment = Sidekiq.options[:environment] || ENV['RAILS_ENV'] || ENV['RACK_ENV'] || 'development'
304
+ environment = Sidekiq.options[:environment] || ENV["APP_ENV"] || ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "development"
241
305
 
242
306
  "[#{environment.upcase}] " unless environment == "production"
243
307
  end
@@ -246,27 +310,31 @@ module Sidekiq
246
310
  "Sidekiq v#{Sidekiq::VERSION}"
247
311
  end
248
312
 
313
+ def server_utc_time
314
+ Time.now.utc.strftime("%H:%M:%S UTC")
315
+ end
316
+
249
317
  def redis_connection_and_namespace
250
318
  @redis_connection_and_namespace ||= begin
251
- namespace_suffix = namespace == nil ? '' : "##{namespace}"
319
+ namespace_suffix = namespace.nil? ? "" : "##{namespace}"
252
320
  "#{redis_connection}#{namespace_suffix}"
253
321
  end
254
322
  end
255
323
 
256
324
  def retry_or_delete_or_kill(job, params)
257
- if params['retry']
325
+ if params["retry"]
258
326
  job.retry
259
- elsif params['delete']
327
+ elsif params["delete"]
260
328
  job.delete
261
- elsif params['kill']
329
+ elsif params["kill"]
262
330
  job.kill
263
331
  end
264
332
  end
265
333
 
266
334
  def delete_or_add_queue(job, params)
267
- if params['delete']
335
+ if params["delete"]
268
336
  job.delete
269
- elsif params['add_to_queue']
337
+ elsif params["add_to_queue"]
270
338
  job.add_to_queue
271
339
  end
272
340
  end
@@ -1,18 +1,23 @@
1
1
  # frozen_string_literal: true
2
- require 'rack'
2
+
3
+ require "rack"
3
4
 
4
5
  module Sidekiq
5
6
  module WebRouter
6
- GET = 'GET'.freeze
7
- DELETE = 'DELETE'.freeze
8
- POST = 'POST'.freeze
9
- PUT = 'PUT'.freeze
10
- PATCH = 'PATCH'.freeze
11
- HEAD = 'HEAD'.freeze
12
-
13
- ROUTE_PARAMS = 'rack.route_params'.freeze
14
- REQUEST_METHOD = 'REQUEST_METHOD'.freeze
15
- PATH_INFO = 'PATH_INFO'.freeze
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
16
21
 
17
22
  def get(path, &block)
18
23
  route(GET, path, &block)
@@ -35,18 +40,22 @@ module Sidekiq
35
40
  end
36
41
 
37
42
  def route(method, path, &block)
38
- @routes ||= { GET => [], POST => [], PUT => [], PATCH => [], DELETE => [], HEAD => [] }
43
+ @routes ||= {GET => [], POST => [], PUT => [], PATCH => [], DELETE => [], HEAD => []}
39
44
 
40
45
  @routes[method] << WebRoute.new(method, path, block)
41
- @routes[HEAD] << WebRoute.new(method, path, block) if method == GET
42
46
  end
43
47
 
44
48
  def match(env)
45
49
  request_method = env[REQUEST_METHOD]
46
50
  path_info = ::Rack::Utils.unescape env[PATH_INFO]
47
51
 
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 == ""
55
+
48
56
  @routes[request_method].each do |route|
49
- if params = route.match(request_method, path_info)
57
+ params = route.match(request_method, path_info)
58
+ if params
50
59
  env[ROUTE_PARAMS] = params
51
60
 
52
61
  return WebAction.new(env, route.block)
@@ -60,7 +69,7 @@ module Sidekiq
60
69
  class WebRoute
61
70
  attr_accessor :request_method, :pattern, :block, :name
62
71
 
63
- NAMED_SEGMENTS_PATTERN = /\/([^\/]*):([^\.:$\/]+)/.freeze
72
+ NAMED_SEGMENTS_PATTERN = /\/([^\/]*):([^.:$\/]+)/
64
73
 
65
74
  def initialize(request_method, pattern, block)
66
75
  @request_method = request_method
@@ -73,7 +82,7 @@ module Sidekiq
73
82
  end
74
83
 
75
84
  def compile
76
- if pattern.match(NAMED_SEGMENTS_PATTERN)
85
+ if pattern.match?(NAMED_SEGMENTS_PATTERN)
77
86
  p = pattern.gsub(NAMED_SEGMENTS_PATTERN, '/\1(?<\2>[^$/]+)')
78
87
 
79
88
  Regexp.new("\\A#{p}\\Z")
@@ -87,9 +96,8 @@ module Sidekiq
87
96
  when String
88
97
  {} if path == matcher
89
98
  else
90
- if path_match = path.match(matcher)
91
- Hash[path_match.names.map(&:to_sym).zip(path_match.captures)]
92
- end
99
+ path_match = path.match(matcher)
100
+ path_match&.named_captures&.transform_keys(&:to_sym)
93
101
  end
94
102
  end
95
103
  end