sidekiq 6.0.0 → 6.1.1

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 (66) hide show
  1. checksums.yaml +4 -4
  2. data/.circleci/config.yml +13 -3
  3. data/6.0-Upgrade.md +3 -1
  4. data/Changes.md +155 -1
  5. data/Ent-Changes.md +27 -2
  6. data/Gemfile +2 -2
  7. data/Gemfile.lock +121 -109
  8. data/Pro-Changes.md +29 -2
  9. data/README.md +4 -5
  10. data/bin/sidekiq +26 -2
  11. data/bin/sidekiqload +8 -4
  12. data/bin/sidekiqmon +4 -5
  13. data/lib/generators/sidekiq/worker_generator.rb +11 -1
  14. data/lib/sidekiq/api.rb +130 -94
  15. data/lib/sidekiq/cli.rb +40 -24
  16. data/lib/sidekiq/client.rb +33 -12
  17. data/lib/sidekiq/extensions/active_record.rb +3 -2
  18. data/lib/sidekiq/extensions/class_methods.rb +5 -4
  19. data/lib/sidekiq/fetch.rb +26 -26
  20. data/lib/sidekiq/job_logger.rb +12 -4
  21. data/lib/sidekiq/job_retry.rb +23 -10
  22. data/lib/sidekiq/launcher.rb +35 -10
  23. data/lib/sidekiq/logger.rb +108 -12
  24. data/lib/sidekiq/manager.rb +3 -3
  25. data/lib/sidekiq/middleware/chain.rb +11 -2
  26. data/lib/sidekiq/monitor.rb +3 -18
  27. data/lib/sidekiq/paginator.rb +7 -2
  28. data/lib/sidekiq/processor.rb +22 -24
  29. data/lib/sidekiq/rails.rb +16 -18
  30. data/lib/sidekiq/redis_connection.rb +21 -13
  31. data/lib/sidekiq/scheduled.rb +13 -12
  32. data/lib/sidekiq/sd_notify.rb +149 -0
  33. data/lib/sidekiq/systemd.rb +24 -0
  34. data/lib/sidekiq/testing.rb +13 -1
  35. data/lib/sidekiq/util.rb +0 -2
  36. data/lib/sidekiq/version.rb +1 -1
  37. data/lib/sidekiq/web/application.rb +22 -21
  38. data/lib/sidekiq/web/csrf_protection.rb +153 -0
  39. data/lib/sidekiq/web/helpers.rb +25 -16
  40. data/lib/sidekiq/web/router.rb +2 -4
  41. data/lib/sidekiq/web.rb +16 -8
  42. data/lib/sidekiq/worker.rb +8 -11
  43. data/lib/sidekiq.rb +21 -7
  44. data/sidekiq.gemspec +3 -4
  45. data/web/assets/javascripts/application.js +25 -27
  46. data/web/assets/javascripts/dashboard.js +2 -2
  47. data/web/assets/stylesheets/application-dark.css +133 -0
  48. data/web/assets/stylesheets/application.css +14 -0
  49. data/web/locales/de.yml +14 -2
  50. data/web/locales/en.yml +2 -0
  51. data/web/locales/fr.yml +2 -2
  52. data/web/locales/ja.yml +2 -0
  53. data/web/locales/lt.yml +83 -0
  54. data/web/locales/pl.yml +4 -4
  55. data/web/locales/vi.yml +83 -0
  56. data/web/views/_job_info.erb +2 -1
  57. data/web/views/busy.erb +4 -1
  58. data/web/views/dead.erb +2 -2
  59. data/web/views/layout.erb +1 -0
  60. data/web/views/morgue.erb +4 -1
  61. data/web/views/queue.erb +10 -1
  62. data/web/views/queues.erb +8 -0
  63. data/web/views/retries.erb +4 -1
  64. data/web/views/retry.erb +2 -2
  65. data/web/views/scheduled.erb +4 -1
  66. metadata +16 -24
@@ -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.1"
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,36 +290,30 @@ 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
301
  resp = 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
316
 
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
317
  resp
317
318
  end
318
319
 
@@ -0,0 +1,153 @@
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
+ localtoken = sess[:csrf]
94
+
95
+ # Rotate the session token after every use
96
+ sess[:csrf] = SecureRandom.base64(TOKEN_LENGTH)
97
+
98
+ # See if it's actually a masked token or not. We should be able
99
+ # to handle any unmasked tokens that we've issued without error.
100
+
101
+ if unmasked_token?(token)
102
+ compare_with_real_token token, localtoken
103
+ elsif masked_token?(token)
104
+ unmasked = unmask_token(token)
105
+ compare_with_real_token unmasked, localtoken
106
+ else
107
+ false # Token is malformed
108
+ end
109
+ end
110
+
111
+ # Creates a masked version of the authenticity token that varies
112
+ # on each request. The masking is used to mitigate SSL attacks
113
+ # like BREACH.
114
+ def mask_token(token)
115
+ token = decode_token(token)
116
+ one_time_pad = SecureRandom.random_bytes(token.length)
117
+ encrypted_token = xor_byte_strings(one_time_pad, token)
118
+ masked_token = one_time_pad + encrypted_token
119
+ Base64.strict_encode64(masked_token)
120
+ end
121
+
122
+ # Essentially the inverse of +mask_token+.
123
+ def unmask_token(masked_token)
124
+ # Split the token into the one-time pad and the encrypted
125
+ # value and decrypt it
126
+ token_length = masked_token.length / 2
127
+ one_time_pad = masked_token[0...token_length]
128
+ encrypted_token = masked_token[token_length..-1]
129
+ xor_byte_strings(one_time_pad, encrypted_token)
130
+ end
131
+
132
+ def unmasked_token?(token)
133
+ token.length == TOKEN_LENGTH
134
+ end
135
+
136
+ def masked_token?(token)
137
+ token.length == TOKEN_LENGTH * 2
138
+ end
139
+
140
+ def compare_with_real_token(token, local)
141
+ Rack::Utils.secure_compare(token.to_s, decode_token(local).to_s)
142
+ end
143
+
144
+ def decode_token(token)
145
+ Base64.strict_decode64(token)
146
+ end
147
+
148
+ def xor_byte_strings(s1, s2)
149
+ s1.bytes.zip(s2.bytes).map { |(c1, c2)| c1 ^ c2 }.pack("c*")
150
+ end
151
+ end
152
+ end
153
+ 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
@@ -48,8 +48,8 @@ module Sidekiq
48
48
  # In practice, any option is allowed. This is the main mechanism to configure the
49
49
  # options for a specific job.
50
50
  def sidekiq_options(opts = {})
51
- opts = Hash[opts.map { |k, v| [k.to_s, v] }] # stringify
52
- self.sidekiq_options_hash = get_sidekiq_options.merge(Hash[opts.map { |k, v| [k.to_s, v] }])
51
+ opts = opts.transform_keys(&:to_s) # stringify
52
+ self.sidekiq_options_hash = get_sidekiq_options.merge(opts)
53
53
  end
54
54
 
55
55
  def sidekiq_retry_in(&block)
@@ -171,9 +171,9 @@ module Sidekiq
171
171
  now = Time.now.to_f
172
172
  ts = (int < 1_000_000_000 ? now + int : int)
173
173
 
174
- payload = @opts.merge("class" => @klass, "args" => args, "at" => ts)
174
+ payload = @opts.merge("class" => @klass, "args" => args)
175
175
  # Optimization to enqueue something now that is scheduled to go out now or in the past
176
- payload.delete("at") if ts <= now
176
+ payload["at"] = ts if ts > now
177
177
  @klass.client_push(payload)
178
178
  end
179
179
  alias_method :perform_at, :perform_in
@@ -207,10 +207,10 @@ module Sidekiq
207
207
  now = Time.now.to_f
208
208
  ts = (int < 1_000_000_000 ? now + int : int)
209
209
 
210
- item = {"class" => self, "args" => args, "at" => ts}
210
+ item = {"class" => self, "args" => args}
211
211
 
212
212
  # Optimization to enqueue something now that is scheduled to go out now or in the past
213
- item.delete("at") if ts <= now
213
+ item["at"] = ts if ts > now
214
214
 
215
215
  client_push(item)
216
216
  end
@@ -235,12 +235,9 @@ module Sidekiq
235
235
 
236
236
  def client_push(item) # :nodoc:
237
237
  pool = Thread.current[:sidekiq_via_pool] || get_sidekiq_options["pool"] || Sidekiq.redis_pool
238
- # stringify
239
- item.keys.each do |key|
240
- item[key.to_s] = item.delete(key)
241
- end
238
+ stringified_item = item.transform_keys(&:to_s)
242
239
 
243
- Sidekiq::Client.new(pool).push(item)
240
+ Sidekiq::Client.new(pool).push(stringified_item)
244
241
  end
245
242
  end
246
243
  end