sidekiq 6.0.0 → 6.1.2

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 (71) hide show
  1. checksums.yaml +4 -4
  2. data/.github/ISSUE_TEMPLATE/bug_report.md +20 -0
  3. data/.github/workflows/ci.yml +41 -0
  4. data/6.0-Upgrade.md +3 -1
  5. data/Changes.md +163 -1
  6. data/Ent-Changes.md +33 -2
  7. data/Gemfile +2 -2
  8. data/Gemfile.lock +109 -113
  9. data/Pro-Changes.md +39 -2
  10. data/README.md +4 -6
  11. data/bin/sidekiq +26 -2
  12. data/bin/sidekiqload +8 -4
  13. data/bin/sidekiqmon +4 -5
  14. data/lib/generators/sidekiq/worker_generator.rb +11 -1
  15. data/lib/sidekiq/api.rb +130 -94
  16. data/lib/sidekiq/cli.rb +40 -24
  17. data/lib/sidekiq/client.rb +33 -12
  18. data/lib/sidekiq/extensions/action_mailer.rb +3 -2
  19. data/lib/sidekiq/extensions/active_record.rb +4 -3
  20. data/lib/sidekiq/extensions/class_methods.rb +5 -4
  21. data/lib/sidekiq/fetch.rb +26 -26
  22. data/lib/sidekiq/job_logger.rb +12 -4
  23. data/lib/sidekiq/job_retry.rb +23 -10
  24. data/lib/sidekiq/launcher.rb +35 -10
  25. data/lib/sidekiq/logger.rb +108 -12
  26. data/lib/sidekiq/manager.rb +4 -4
  27. data/lib/sidekiq/middleware/chain.rb +12 -3
  28. data/lib/sidekiq/monitor.rb +3 -18
  29. data/lib/sidekiq/paginator.rb +7 -2
  30. data/lib/sidekiq/processor.rb +22 -24
  31. data/lib/sidekiq/rails.rb +16 -18
  32. data/lib/sidekiq/redis_connection.rb +21 -13
  33. data/lib/sidekiq/scheduled.rb +13 -12
  34. data/lib/sidekiq/sd_notify.rb +149 -0
  35. data/lib/sidekiq/systemd.rb +24 -0
  36. data/lib/sidekiq/testing.rb +13 -1
  37. data/lib/sidekiq/util.rb +0 -2
  38. data/lib/sidekiq/version.rb +1 -1
  39. data/lib/sidekiq/web/application.rb +23 -24
  40. data/lib/sidekiq/web/csrf_protection.rb +158 -0
  41. data/lib/sidekiq/web/helpers.rb +25 -16
  42. data/lib/sidekiq/web/router.rb +2 -4
  43. data/lib/sidekiq/web.rb +16 -8
  44. data/lib/sidekiq/worker.rb +8 -11
  45. data/lib/sidekiq.rb +22 -8
  46. data/sidekiq.gemspec +3 -4
  47. data/web/assets/javascripts/application.js +25 -27
  48. data/web/assets/javascripts/dashboard.js +2 -2
  49. data/web/assets/stylesheets/application-dark.css +143 -0
  50. data/web/assets/stylesheets/application.css +16 -6
  51. data/web/locales/de.yml +14 -2
  52. data/web/locales/en.yml +2 -0
  53. data/web/locales/fr.yml +2 -2
  54. data/web/locales/ja.yml +2 -0
  55. data/web/locales/lt.yml +83 -0
  56. data/web/locales/pl.yml +4 -4
  57. data/web/locales/ru.yml +4 -0
  58. data/web/locales/vi.yml +83 -0
  59. data/web/views/_job_info.erb +2 -1
  60. data/web/views/busy.erb +6 -3
  61. data/web/views/dead.erb +2 -2
  62. data/web/views/layout.erb +1 -0
  63. data/web/views/morgue.erb +5 -2
  64. data/web/views/queue.erb +10 -1
  65. data/web/views/queues.erb +9 -1
  66. data/web/views/retries.erb +5 -2
  67. data/web/views/retry.erb +2 -2
  68. data/web/views/scheduled.erb +5 -2
  69. metadata +21 -29
  70. data/.circleci/config.yml +0 -61
  71. data/.github/issue_template.md +0 -11
@@ -0,0 +1,149 @@
1
+ # frozen_string_literal: true
2
+
3
+ # The MIT License
4
+ #
5
+ # Copyright (c) 2017, 2018, 2019, 2020 Agis Anastasopoulos
6
+ #
7
+ # Permission is hereby granted, free of charge, to any person obtaining a copy of
8
+ # this software and associated documentation files (the "Software"), to deal in
9
+ # the Software without restriction, including without limitation the rights to
10
+ # use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
11
+ # the Software, and to permit persons to whom the Software is furnished to do so,
12
+ # subject to the following conditions:
13
+ #
14
+ # The above copyright notice and this permission notice shall be included in all
15
+ # copies or substantial portions of the Software.
16
+ #
17
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
19
+ # FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
20
+ # COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
21
+ # IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
22
+ # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23
+
24
+ # This is a copy of https://github.com/agis/ruby-sdnotify as of commit a7d52ee
25
+ # The only changes made was "rehoming" it within the Sidekiq module to avoid
26
+ # namespace collisions and applying standard's code formatting style.
27
+
28
+ require "socket"
29
+
30
+ # SdNotify is a pure-Ruby implementation of sd_notify(3). It can be used to
31
+ # notify systemd about state changes. Methods of this package are no-op on
32
+ # non-systemd systems (eg. Darwin).
33
+ #
34
+ # The API maps closely to the original implementation of sd_notify(3),
35
+ # therefore be sure to check the official man pages prior to using SdNotify.
36
+ #
37
+ # @see https://www.freedesktop.org/software/systemd/man/sd_notify.html
38
+ module Sidekiq
39
+ module SdNotify
40
+ # Exception raised when there's an error writing to the notification socket
41
+ class NotifyError < RuntimeError; end
42
+
43
+ READY = "READY=1"
44
+ RELOADING = "RELOADING=1"
45
+ STOPPING = "STOPPING=1"
46
+ STATUS = "STATUS="
47
+ ERRNO = "ERRNO="
48
+ MAINPID = "MAINPID="
49
+ WATCHDOG = "WATCHDOG=1"
50
+ FDSTORE = "FDSTORE=1"
51
+
52
+ def self.ready(unset_env = false)
53
+ notify(READY, unset_env)
54
+ end
55
+
56
+ def self.reloading(unset_env = false)
57
+ notify(RELOADING, unset_env)
58
+ end
59
+
60
+ def self.stopping(unset_env = false)
61
+ notify(STOPPING, unset_env)
62
+ end
63
+
64
+ # @param status [String] a custom status string that describes the current
65
+ # state of the service
66
+ def self.status(status, unset_env = false)
67
+ notify("#{STATUS}#{status}", unset_env)
68
+ end
69
+
70
+ # @param errno [Integer]
71
+ def self.errno(errno, unset_env = false)
72
+ notify("#{ERRNO}#{errno}", unset_env)
73
+ end
74
+
75
+ # @param pid [Integer]
76
+ def self.mainpid(pid, unset_env = false)
77
+ notify("#{MAINPID}#{pid}", unset_env)
78
+ end
79
+
80
+ def self.watchdog(unset_env = false)
81
+ notify(WATCHDOG, unset_env)
82
+ end
83
+
84
+ def self.fdstore(unset_env = false)
85
+ notify(FDSTORE, unset_env)
86
+ end
87
+
88
+ # @return [Boolean] true if the service manager expects watchdog keep-alive
89
+ # notification messages to be sent from this process.
90
+ #
91
+ # If the $WATCHDOG_USEC environment variable is set,
92
+ # and the $WATCHDOG_PID variable is unset or set to the PID of the current
93
+ # process
94
+ #
95
+ # @note Unlike sd_watchdog_enabled(3), this method does not mutate the
96
+ # environment.
97
+ def self.watchdog?
98
+ wd_usec = ENV["WATCHDOG_USEC"]
99
+ wd_pid = ENV["WATCHDOG_PID"]
100
+
101
+ return false unless wd_usec
102
+
103
+ begin
104
+ wd_usec = Integer(wd_usec)
105
+ rescue
106
+ return false
107
+ end
108
+
109
+ return false if wd_usec <= 0
110
+ return true if !wd_pid || wd_pid == $$.to_s
111
+
112
+ false
113
+ end
114
+
115
+ # Notify systemd with the provided state, via the notification socket, if
116
+ # any.
117
+ #
118
+ # Generally this method will be used indirectly through the other methods
119
+ # of the library.
120
+ #
121
+ # @param state [String]
122
+ # @param unset_env [Boolean]
123
+ #
124
+ # @return [Fixnum, nil] the number of bytes written to the notification
125
+ # socket or nil if there was no socket to report to (eg. the program wasn't
126
+ # started by systemd)
127
+ #
128
+ # @raise [NotifyError] if there was an error communicating with the systemd
129
+ # socket
130
+ #
131
+ # @see https://www.freedesktop.org/software/systemd/man/sd_notify.html
132
+ def self.notify(state, unset_env = false)
133
+ sock = ENV["NOTIFY_SOCKET"]
134
+
135
+ return nil unless sock
136
+
137
+ ENV.delete("NOTIFY_SOCKET") if unset_env
138
+
139
+ begin
140
+ Addrinfo.unix(sock, :DGRAM).connect do |s|
141
+ s.close_on_exec = true
142
+ s.write(state)
143
+ end
144
+ rescue => e
145
+ raise NotifyError, "#{e.class}: #{e.message}", e.backtrace
146
+ end
147
+ end
148
+ end
149
+ end
@@ -0,0 +1,24 @@
1
+ #
2
+ # Sidekiq's systemd integration allows Sidekiq to inform systemd:
3
+ # 1. when it has successfully started
4
+ # 2. when it is starting shutdown
5
+ # 3. periodically for a liveness check with a watchdog thread
6
+ #
7
+ module Sidekiq
8
+ def self.start_watchdog
9
+ usec = Integer(ENV["WATCHDOG_USEC"])
10
+ return Sidekiq.logger.error("systemd Watchdog too fast: " + usec) if usec < 1_000_000
11
+
12
+ sec_f = usec / 1_000_000.0
13
+ # "It is recommended that a daemon sends a keep-alive notification message
14
+ # to the service manager every half of the time returned here."
15
+ ping_f = sec_f / 2
16
+ Sidekiq.logger.info "Pinging systemd watchdog every #{ping_f.round(1)} sec"
17
+ Thread.new do
18
+ loop do
19
+ sleep ping_f
20
+ Sidekiq::SdNotify.watchdog
21
+ end
22
+ end
23
+ end
24
+ end
@@ -323,9 +323,21 @@ module Sidekiq
323
323
  end
324
324
  end
325
325
  end
326
+
327
+ module TestingExtensions
328
+ def jobs_for(klass)
329
+ jobs.select do |job|
330
+ marshalled = job["args"][0]
331
+ marshalled.index(klass.to_s) && YAML.load(marshalled)[0] == klass
332
+ end
333
+ end
334
+ end
335
+
336
+ Sidekiq::Extensions::DelayedMailer.extend(TestingExtensions) if defined?(Sidekiq::Extensions::DelayedMailer)
337
+ Sidekiq::Extensions::DelayedModel.extend(TestingExtensions) if defined?(Sidekiq::Extensions::DelayedModel)
326
338
  end
327
339
 
328
- if defined?(::Rails) && Rails.respond_to?(:env) && !Rails.env.test?
340
+ if defined?(::Rails) && Rails.respond_to?(:env) && !Rails.env.test? && !$TESTING
329
341
  puts("**************************************************")
330
342
  puts("⛔️ WARNING: Sidekiq testing API enabled, but this is not the test environment. Your jobs will not go to Redis.")
331
343
  puts("**************************************************")
data/lib/sidekiq/util.rb CHANGED
@@ -11,8 +11,6 @@ module Sidekiq
11
11
  module Util
12
12
  include ExceptionHandler
13
13
 
14
- EXPIRY = 60 * 60 * 24
15
-
16
14
  def watchdog(last_words)
17
15
  yield
18
16
  rescue Exception => ex
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Sidekiq
4
- VERSION = "6.0.0"
4
+ VERSION = "6.1.2"
5
5
  end
@@ -5,7 +5,6 @@ module Sidekiq
5
5
  extend WebRouter
6
6
 
7
7
  CONTENT_LENGTH = "Content-Length"
8
- CONTENT_TYPE = "Content-Type"
9
8
  REDIS_KEYS = %w[redis_version uptime_in_days connected_clients used_memory_human used_memory_peak_human]
10
9
  CSP_HEADER = [
11
10
  "default-src 'self' https: http:",
@@ -20,7 +19,7 @@ module Sidekiq
20
19
  "script-src 'self' https: http: 'unsafe-inline'",
21
20
  "style-src 'self' https: http: 'unsafe-inline'",
22
21
  "worker-src 'self'",
23
- "base-uri 'self'",
22
+ "base-uri 'self'"
24
23
  ].join("; ").freeze
25
24
 
26
25
  def initialize(klass)
@@ -84,14 +83,22 @@ module Sidekiq
84
83
 
85
84
  @count = (params["count"] || 25).to_i
86
85
  @queue = Sidekiq::Queue.new(@name)
87
- (@current_page, @total_size, @messages) = page("queue:#{@name}", params["page"], @count)
86
+ (@current_page, @total_size, @messages) = page("queue:#{@name}", params["page"], @count, reverse: params["direction"] == "asc")
88
87
  @messages = @messages.map { |msg| Sidekiq::Job.new(msg, @name) }
89
88
 
90
89
  erb(:queue)
91
90
  end
92
91
 
93
92
  post "/queues/:name" do
94
- Sidekiq::Queue.new(route_params[:name]).clear
93
+ queue = Sidekiq::Queue.new(route_params[:name])
94
+
95
+ if Sidekiq.pro? && params["pause"]
96
+ queue.pause!
97
+ elsif Sidekiq.pro? && params["unpause"]
98
+ queue.unpause!
99
+ else
100
+ queue.clear
101
+ end
95
102
 
96
103
  redirect "#{root_path}queues"
97
104
  end
@@ -268,7 +275,7 @@ module Sidekiq
268
275
  scheduled: sidekiq_stats.scheduled_size,
269
276
  retries: sidekiq_stats.retry_size,
270
277
  dead: sidekiq_stats.dead_size,
271
- default_latency: sidekiq_stats.default_queue_latency,
278
+ default_latency: sidekiq_stats.default_queue_latency
272
279
  },
273
280
  redis: redis_stats,
274
281
  server_utc_time: server_utc_time
@@ -283,37 +290,29 @@ module Sidekiq
283
290
  action = self.class.match(env)
284
291
  return [404, {"Content-Type" => "text/plain", "X-Cascade" => "pass"}, ["Not Found"]] unless action
285
292
 
286
- resp = catch(:halt) {
287
- app = @klass
293
+ app = @klass
294
+ resp = catch(:halt) do # rubocop:disable Standard/SemanticBlocks
288
295
  self.class.run_befores(app, action)
289
- begin
290
- resp = action.instance_exec env, &action.block
291
- ensure
292
- self.class.run_afters(app, action)
293
- end
294
-
295
- resp
296
- }
296
+ action.instance_exec env, &action.block
297
+ ensure
298
+ self.class.run_afters(app, action)
299
+ end
297
300
 
298
- resp = case resp
301
+ case resp
299
302
  when Array
303
+ # redirects go here
300
304
  resp
301
305
  else
306
+ # rendered content goes here
302
307
  headers = {
303
308
  "Content-Type" => "text/html",
304
309
  "Cache-Control" => "no-cache",
305
310
  "Content-Language" => action.locale,
306
- "Content-Security-Policy" => CSP_HEADER,
311
+ "Content-Security-Policy" => CSP_HEADER
307
312
  }
308
-
313
+ # we'll let Rack calculate Content-Length for us.
309
314
  [200, headers, [resp]]
310
315
  end
311
-
312
- resp[1] = resp[1].dup
313
-
314
- resp[1][CONTENT_LENGTH] = resp[2].inject(0) { |l, p| l + p.bytesize }.to_s
315
-
316
- resp
317
316
  end
318
317
 
319
318
  def self.helpers(mod = nil, &block)
@@ -0,0 +1,158 @@
1
+ # frozen_string_literal: true
2
+
3
+ # this file originally based on authenticity_token.rb from the sinatra/rack-protection project
4
+ #
5
+ # The MIT License (MIT)
6
+ #
7
+ # Copyright (c) 2011-2017 Konstantin Haase
8
+ # Copyright (c) 2015-2017 Zachary Scott
9
+ #
10
+ # Permission is hereby granted, free of charge, to any person obtaining
11
+ # a copy of this software and associated documentation files (the
12
+ # 'Software'), to deal in the Software without restriction, including
13
+ # without limitation the rights to use, copy, modify, merge, publish,
14
+ # distribute, sublicense, and/or sell copies of the Software, and to
15
+ # permit persons to whom the Software is furnished to do so, subject to
16
+ # the following conditions:
17
+ #
18
+ # The above copyright notice and this permission notice shall be
19
+ # included in all copies or substantial portions of the Software.
20
+ #
21
+ # THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
22
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
23
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
24
+ # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
25
+ # CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
26
+ # TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
27
+ # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
28
+
29
+ require "securerandom"
30
+ require "base64"
31
+ require "rack/request"
32
+
33
+ module Sidekiq
34
+ class Web
35
+ class CsrfProtection
36
+ def initialize(app, options = nil)
37
+ @app = app
38
+ end
39
+
40
+ def call(env)
41
+ accept?(env) ? admit(env) : deny(env)
42
+ end
43
+
44
+ private
45
+
46
+ def admit(env)
47
+ # On each successful request, we create a fresh masked token
48
+ # which will be used in any forms rendered for this request.
49
+ s = session(env)
50
+ s[:csrf] ||= SecureRandom.base64(TOKEN_LENGTH)
51
+ env[:csrf_token] = mask_token(s[:csrf])
52
+ @app.call(env)
53
+ end
54
+
55
+ def safe?(env)
56
+ %w[GET HEAD OPTIONS TRACE].include? env["REQUEST_METHOD"]
57
+ end
58
+
59
+ def logger(env)
60
+ @logger ||= (env["rack.logger"] || ::Logger.new(env["rack.errors"]))
61
+ end
62
+
63
+ def deny(env)
64
+ logger(env).warn "attack prevented by #{self.class}"
65
+ [403, {"Content-Type" => "text/plain"}, ["Forbidden"]]
66
+ end
67
+
68
+ def session(env)
69
+ env["rack.session"] || fail("you need to set up a session middleware *before* #{self.class}")
70
+ end
71
+
72
+ def accept?(env)
73
+ return true if safe?(env)
74
+
75
+ giventoken = ::Rack::Request.new(env).params["authenticity_token"]
76
+ valid_token?(env, giventoken)
77
+ end
78
+
79
+ TOKEN_LENGTH = 32
80
+
81
+ # Checks that the token given to us as a parameter matches
82
+ # the token stored in the session.
83
+ def valid_token?(env, giventoken)
84
+ return false if giventoken.nil? || giventoken.empty?
85
+
86
+ begin
87
+ token = decode_token(giventoken)
88
+ rescue ArgumentError # client input is invalid
89
+ return false
90
+ end
91
+
92
+ sess = session(env)
93
+
94
+ # Checks that Rack::Session::Cookie did not return empty session
95
+ # object in case the digest verification failed
96
+ return false if sess.empty?
97
+
98
+ localtoken = sess[:csrf]
99
+
100
+ # Rotate the session token after every use
101
+ sess[:csrf] = SecureRandom.base64(TOKEN_LENGTH)
102
+
103
+ # See if it's actually a masked token or not. We should be able
104
+ # to handle any unmasked tokens that we've issued without error.
105
+
106
+ if unmasked_token?(token)
107
+ compare_with_real_token token, localtoken
108
+ elsif masked_token?(token)
109
+ unmasked = unmask_token(token)
110
+ compare_with_real_token unmasked, localtoken
111
+ else
112
+ false # Token is malformed
113
+ end
114
+ end
115
+
116
+ # Creates a masked version of the authenticity token that varies
117
+ # on each request. The masking is used to mitigate SSL attacks
118
+ # like BREACH.
119
+ def mask_token(token)
120
+ token = decode_token(token)
121
+ one_time_pad = SecureRandom.random_bytes(token.length)
122
+ encrypted_token = xor_byte_strings(one_time_pad, token)
123
+ masked_token = one_time_pad + encrypted_token
124
+ Base64.strict_encode64(masked_token)
125
+ end
126
+
127
+ # Essentially the inverse of +mask_token+.
128
+ def unmask_token(masked_token)
129
+ # Split the token into the one-time pad and the encrypted
130
+ # value and decrypt it
131
+ token_length = masked_token.length / 2
132
+ one_time_pad = masked_token[0...token_length]
133
+ encrypted_token = masked_token[token_length..-1]
134
+ xor_byte_strings(one_time_pad, encrypted_token)
135
+ end
136
+
137
+ def unmasked_token?(token)
138
+ token.length == TOKEN_LENGTH
139
+ end
140
+
141
+ def masked_token?(token)
142
+ token.length == TOKEN_LENGTH * 2
143
+ end
144
+
145
+ def compare_with_real_token(token, local)
146
+ ::Rack::Utils.secure_compare(token.to_s, decode_token(local).to_s)
147
+ end
148
+
149
+ def decode_token(token)
150
+ Base64.strict_decode64(token)
151
+ end
152
+
153
+ def xor_byte_strings(s1, s2)
154
+ s1.bytes.zip(s2.bytes).map { |(c1, c2)| c1 ^ c2 }.pack("c*")
155
+ end
156
+ end
157
+ end
158
+ end
@@ -65,7 +65,10 @@ module Sidekiq
65
65
 
66
66
  def poll_path
67
67
  if current_path != "" && params["poll"]
68
- root_path + current_path
68
+ path = root_path + current_path
69
+ query_string = to_query_string(params.slice(*params.keys - %w[page poll]))
70
+ path += "?#{query_string}" unless query_string.empty?
71
+ path
69
72
  else
70
73
  ""
71
74
  end
@@ -112,6 +115,13 @@ module Sidekiq
112
115
  end
113
116
  end
114
117
 
118
+ # within is used by Sidekiq Pro
119
+ def display_tags(job, within = nil)
120
+ job.tags.map { |tag|
121
+ "<span class='jobtag label label-info'>#{::Rack::Utils.escape_html(tag)}</span>"
122
+ }.join(" ")
123
+ end
124
+
115
125
  # mperham/sidekiq#3243
116
126
  def unfiltered?
117
127
  yield unless env["PATH_INFO"].start_with?("/filter/")
@@ -130,6 +140,10 @@ module Sidekiq
130
140
  end
131
141
  end
132
142
 
143
+ def sort_direction_label
144
+ params[:direction] == "asc" ? "&uarr;" : "&darr;"
145
+ end
146
+
133
147
  def workers
134
148
  @workers ||= Sidekiq::Workers.new
135
149
  end
@@ -142,12 +156,6 @@ module Sidekiq
142
156
  @stats ||= Sidekiq::Stats.new
143
157
  end
144
158
 
145
- def retries_with_score(score)
146
- Sidekiq.redis { |conn|
147
- conn.zrangebyscore("retry", score, score)
148
- }.map { |msg| Sidekiq.load_json(msg) }
149
- end
150
-
151
159
  def redis_connection
152
160
  Sidekiq.redis do |conn|
153
161
  c = conn.connection
@@ -189,16 +197,17 @@ module Sidekiq
189
197
  [score.to_f, jid]
190
198
  end
191
199
 
192
- SAFE_QPARAMS = %w[page poll]
200
+ SAFE_QPARAMS = %w[page poll direction]
193
201
 
194
202
  # Merge options with current params, filter safe params, and stringify to query string
195
203
  def qparams(options)
196
- # stringify
197
- options.keys.each do |key|
198
- options[key.to_s] = options.delete(key)
199
- end
204
+ stringified_options = options.transform_keys(&:to_s)
205
+
206
+ to_query_string(params.merge(stringified_options))
207
+ end
200
208
 
201
- params.merge(options).map { |key, value|
209
+ def to_query_string(params)
210
+ params.map { |key, value|
202
211
  SAFE_QPARAMS.include?(key) ? "#{key}=#{CGI.escape(value.to_s)}" : next
203
212
  }.compact.join("&")
204
213
  end
@@ -221,7 +230,7 @@ module Sidekiq
221
230
  end
222
231
 
223
232
  def csrf_tag
224
- "<input type='hidden' name='authenticity_token' value='#{session[:csrf]}'/>"
233
+ "<input type='hidden' name='authenticity_token' value='#{env[:csrf_token]}'/>"
225
234
  end
226
235
 
227
236
  def to_display(arg)
@@ -238,7 +247,7 @@ module Sidekiq
238
247
  queue class args retry_count retried_at failed_at
239
248
  jid error_message error_class backtrace
240
249
  error_backtrace enqueued_at retry wrapped
241
- created_at
250
+ created_at tags
242
251
  ])
243
252
 
244
253
  def retry_extra_items(retry_job)
@@ -283,7 +292,7 @@ module Sidekiq
283
292
  end
284
293
 
285
294
  def environment_title_prefix
286
- environment = Sidekiq.options[:environment] || ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "development"
295
+ environment = Sidekiq.options[:environment] || ENV["APP_ENV"] || ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "development"
287
296
 
288
297
  "[#{environment.upcase}] " unless environment == "production"
289
298
  end
@@ -66,7 +66,7 @@ module Sidekiq
66
66
  class WebRoute
67
67
  attr_accessor :request_method, :pattern, :block, :name
68
68
 
69
- NAMED_SEGMENTS_PATTERN = /\/([^\/]*):([^\.:$\/]+)/
69
+ NAMED_SEGMENTS_PATTERN = /\/([^\/]*):([^.:$\/]+)/
70
70
 
71
71
  def initialize(request_method, pattern, block)
72
72
  @request_method = request_method
@@ -94,9 +94,7 @@ module Sidekiq
94
94
  {} if path == matcher
95
95
  else
96
96
  path_match = path.match(matcher)
97
- if path_match
98
- Hash[path_match.names.map(&:to_sym).zip(path_match.captures)]
99
- end
97
+ path_match&.named_captures&.transform_keys(&:to_sym)
100
98
  end
101
99
  end
102
100
  end
data/lib/sidekiq/web.rb CHANGED
@@ -10,8 +10,9 @@ require "sidekiq/web/helpers"
10
10
  require "sidekiq/web/router"
11
11
  require "sidekiq/web/action"
12
12
  require "sidekiq/web/application"
13
+ require "sidekiq/web/csrf_protection"
13
14
 
14
- require "rack/protection"
15
+ require "rack/content_length"
15
16
 
16
17
  require "rack/builder"
17
18
  require "rack/file"
@@ -31,7 +32,7 @@ module Sidekiq
31
32
  "Queues" => "queues",
32
33
  "Retries" => "retries",
33
34
  "Scheduled" => "scheduled",
34
- "Dead" => "morgue",
35
+ "Dead" => "morgue"
35
36
  }
36
37
 
37
38
  class << self
@@ -154,14 +155,14 @@ module Sidekiq
154
155
  def build_sessions
155
156
  middlewares = self.middlewares
156
157
 
157
- unless using?(::Rack::Protection) || ENV["RACK_ENV"] == "test"
158
- middlewares.unshift [[::Rack::Protection, {use: :authenticity_token}], nil]
159
- end
160
-
161
158
  s = sessions
162
- return unless s
163
159
 
164
- unless using? ::Rack::Session::Cookie
160
+ # turn on CSRF protection if sessions are enabled and this is not the test env
161
+ if s && !using?(CsrfProtection) && ENV["RACK_ENV"] != "test"
162
+ middlewares.unshift [[CsrfProtection], nil]
163
+ end
164
+
165
+ if s && !using?(::Rack::Session::Cookie)
165
166
  unless (secret = Web.session_secret)
166
167
  require "securerandom"
167
168
  secret = SecureRandom.hex(64)
@@ -172,6 +173,13 @@ module Sidekiq
172
173
 
173
174
  middlewares.unshift [[::Rack::Session::Cookie, options], nil]
174
175
  end
176
+
177
+ # Since Sidekiq::WebApplication no longer calculates its own
178
+ # Content-Length response header, we must ensure that the Rack middleware
179
+ # that does this is loaded
180
+ unless using? ::Rack::ContentLength
181
+ middlewares.unshift [[::Rack::ContentLength], nil]
182
+ end
175
183
  end
176
184
 
177
185
  def build