sidekiq 4.2.4 → 5.2.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 (106) hide show
  1. checksums.yaml +4 -4
  2. data/.github/issue_template.md +8 -1
  3. data/.gitignore +1 -0
  4. data/.travis.yml +5 -3
  5. data/5.0-Upgrade.md +56 -0
  6. data/COMM-LICENSE +1 -1
  7. data/Changes.md +151 -0
  8. data/Ent-Changes.md +77 -2
  9. data/Gemfile +10 -25
  10. data/LICENSE +1 -1
  11. data/Pro-4.0-Upgrade.md +35 -0
  12. data/Pro-Changes.md +156 -2
  13. data/README.md +9 -6
  14. data/Rakefile +1 -2
  15. data/bin/sidekiqctl +1 -1
  16. data/bin/sidekiqload +15 -33
  17. data/lib/generators/sidekiq/templates/worker_spec.rb.erb +1 -1
  18. data/lib/generators/sidekiq/templates/worker_test.rb.erb +1 -1
  19. data/lib/sidekiq/api.rb +157 -67
  20. data/lib/sidekiq/cli.rb +71 -26
  21. data/lib/sidekiq/client.rb +25 -18
  22. data/lib/sidekiq/core_ext.rb +1 -106
  23. data/lib/sidekiq/delay.rb +42 -0
  24. data/lib/sidekiq/exception_handler.rb +2 -4
  25. data/lib/sidekiq/extensions/generic_proxy.rb +7 -1
  26. data/lib/sidekiq/fetch.rb +1 -1
  27. data/lib/sidekiq/job_logger.rb +25 -0
  28. data/lib/sidekiq/job_retry.rb +241 -0
  29. data/lib/sidekiq/launcher.rb +45 -37
  30. data/lib/sidekiq/logging.rb +18 -2
  31. data/lib/sidekiq/manager.rb +3 -4
  32. data/lib/sidekiq/middleware/server/active_record.rb +10 -0
  33. data/lib/sidekiq/processor.rb +91 -34
  34. data/lib/sidekiq/rails.rb +15 -51
  35. data/lib/sidekiq/redis_connection.rb +31 -5
  36. data/lib/sidekiq/scheduled.rb +35 -8
  37. data/lib/sidekiq/testing.rb +24 -7
  38. data/lib/sidekiq/util.rb +6 -2
  39. data/lib/sidekiq/version.rb +1 -1
  40. data/lib/sidekiq/web/action.rb +2 -6
  41. data/lib/sidekiq/web/application.rb +28 -21
  42. data/lib/sidekiq/web/helpers.rb +67 -23
  43. data/lib/sidekiq/web/router.rb +14 -10
  44. data/lib/sidekiq/web.rb +4 -4
  45. data/lib/sidekiq/worker.rb +97 -14
  46. data/lib/sidekiq.rb +23 -24
  47. data/sidekiq.gemspec +7 -10
  48. data/web/assets/javascripts/application.js +0 -0
  49. data/web/assets/javascripts/dashboard.js +18 -13
  50. data/web/assets/stylesheets/application-rtl.css +246 -0
  51. data/web/assets/stylesheets/application.css +336 -4
  52. data/web/assets/stylesheets/bootstrap-rtl.min.css +9 -0
  53. data/web/assets/stylesheets/bootstrap.css +2 -2
  54. data/web/locales/ar.yml +80 -0
  55. data/web/locales/en.yml +1 -0
  56. data/web/locales/es.yml +4 -3
  57. data/web/locales/fa.yml +80 -0
  58. data/web/locales/he.yml +79 -0
  59. data/web/locales/ja.yml +5 -3
  60. data/web/locales/ur.yml +80 -0
  61. data/web/views/_footer.erb +5 -2
  62. data/web/views/_job_info.erb +1 -1
  63. data/web/views/_nav.erb +1 -1
  64. data/web/views/_paging.erb +1 -1
  65. data/web/views/busy.erb +9 -5
  66. data/web/views/dashboard.erb +3 -3
  67. data/web/views/layout.erb +11 -2
  68. data/web/views/morgue.erb +14 -10
  69. data/web/views/queue.erb +10 -10
  70. data/web/views/queues.erb +4 -2
  71. data/web/views/retries.erb +13 -11
  72. data/web/views/retry.erb +1 -1
  73. data/web/views/scheduled.erb +2 -2
  74. metadata +26 -160
  75. data/lib/sidekiq/middleware/server/logging.rb +0 -40
  76. data/lib/sidekiq/middleware/server/retry_jobs.rb +0 -205
  77. data/test/config.yml +0 -9
  78. data/test/env_based_config.yml +0 -11
  79. data/test/fake_env.rb +0 -1
  80. data/test/fixtures/en.yml +0 -2
  81. data/test/helper.rb +0 -75
  82. data/test/test_actors.rb +0 -138
  83. data/test/test_api.rb +0 -528
  84. data/test/test_cli.rb +0 -418
  85. data/test/test_client.rb +0 -266
  86. data/test/test_exception_handler.rb +0 -56
  87. data/test/test_extensions.rb +0 -127
  88. data/test/test_fetch.rb +0 -50
  89. data/test/test_launcher.rb +0 -95
  90. data/test/test_logging.rb +0 -35
  91. data/test/test_manager.rb +0 -50
  92. data/test/test_middleware.rb +0 -158
  93. data/test/test_processor.rb +0 -235
  94. data/test/test_rails.rb +0 -22
  95. data/test/test_redis_connection.rb +0 -132
  96. data/test/test_retry.rb +0 -326
  97. data/test/test_retry_exhausted.rb +0 -149
  98. data/test/test_scheduled.rb +0 -115
  99. data/test/test_scheduling.rb +0 -58
  100. data/test/test_sidekiq.rb +0 -107
  101. data/test/test_testing.rb +0 -143
  102. data/test/test_testing_fake.rb +0 -357
  103. data/test/test_testing_inline.rb +0 -94
  104. data/test/test_util.rb +0 -13
  105. data/test/test_web.rb +0 -726
  106. data/test/test_web_helpers.rb +0 -54
data/lib/sidekiq/rails.rb CHANGED
@@ -1,36 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
  module Sidekiq
3
- def self.hook_rails!
4
- return if defined?(@delay_removed)
5
-
6
- ActiveSupport.on_load(:active_record) do
7
- include Sidekiq::Extensions::ActiveRecord
8
- end
9
-
10
- ActiveSupport.on_load(:action_mailer) do
11
- extend Sidekiq::Extensions::ActionMailer
12
- end
13
-
14
- Module.__send__(:include, Sidekiq::Extensions::Klass)
15
- end
16
-
17
- # Removes the generic aliases which MAY clash with names of already
18
- # created methods by other applications. The methods `sidekiq_delay`,
19
- # `sidekiq_delay_for` and `sidekiq_delay_until` can be used instead.
20
- def self.remove_delay!
21
- @delay_removed = true
22
-
23
- [Extensions::ActiveRecord,
24
- Extensions::ActionMailer,
25
- Extensions::Klass].each do |mod|
26
- mod.module_eval do
27
- remove_method :delay if respond_to?(:delay)
28
- remove_method :delay_for if respond_to?(:delay_for)
29
- remove_method :delay_until if respond_to?(:delay_until)
30
- end
31
- end
32
- end
33
-
34
3
  class Rails < ::Rails::Engine
35
4
  # We need to setup this up before any application configuration which might
36
5
  # change Sidekiq middleware.
@@ -48,26 +17,16 @@ module Sidekiq
48
17
  end
49
18
  end
50
19
 
51
- initializer 'sidekiq' do
52
- Sidekiq.hook_rails!
53
- end
54
-
55
- # We have to add the reloader after initialize to see if cache_classes has
56
- # been turned on.
57
- #
58
- # This hook happens after all initialziers are run, just before returning
59
- # from config/environment.rb back to sidekiq/cli.rb.
60
20
  config.after_initialize do
61
- if ::Rails::VERSION::MAJOR >= 5
62
- # The reloader also takes care of ActiveRecord but is incompatible with
63
- # the ActiveRecord middleware so make sure it's not in the chain already.
64
- if defined?(Sidekiq::Middleware::Server::ActiveRecord) && Sidekiq.server_middleware.exists?(Sidekiq::Middleware::Server::ActiveRecord)
65
- raise ArgumentError, "You are using the Sidekiq ActiveRecord middleware and the new Rails 5 reloader which are incompatible. Please remove the ActiveRecord middleware from your Sidekiq middleware configuration."
66
- elsif ::Rails.application.config.cache_classes
67
- # The reloader API has proven to be troublesome under load in production.
68
- # We won't use it at all when classes are cached, see #3154
69
- Sidekiq.logger.debug { "Autoload disabled in #{::Rails.env}, Sidekiq will not reload changed classes" }
70
- else
21
+ # This hook happens after all initializers are run, just before returning
22
+ # from config/environment.rb back to sidekiq/cli.rb.
23
+ # We have to add the reloader after initialize to see if cache_classes has
24
+ # been turned on.
25
+ #
26
+ # None of this matters on the client-side, only within the Sidekiq process itself.
27
+ #
28
+ Sidekiq.configure_server do |_|
29
+ if ::Rails::VERSION::MAJOR >= 5
71
30
  Sidekiq.options[:reloader] = Sidekiq::Rails::Reloader.new
72
31
  end
73
32
  end
@@ -75,7 +34,6 @@ module Sidekiq
75
34
 
76
35
  class Reloader
77
36
  def initialize(app = ::Rails.application)
78
- Sidekiq.logger.debug "Enabling Rails 5+ live code reloading, so hot!" unless app.config.cache_classes
79
37
  @app = app
80
38
  end
81
39
 
@@ -91,3 +49,9 @@ module Sidekiq
91
49
  end
92
50
  end if defined?(::Rails)
93
51
  end
52
+
53
+ if defined?(::Rails) && ::Rails::VERSION::MAJOR < 4
54
+ $stderr.puts("**************************************************")
55
+ $stderr.puts("⛔️ WARNING: Sidekiq server is no longer supported by Rails 3.2 - please ensure your server/workers are updated")
56
+ $stderr.puts("**************************************************")
57
+ end
@@ -8,11 +8,22 @@ module Sidekiq
8
8
  class << self
9
9
 
10
10
  def create(options={})
11
- options = options.symbolize_keys
11
+ options.keys.each do |key|
12
+ options[key.to_sym] = options.delete(key)
13
+ end
12
14
 
15
+ options[:id] = "Sidekiq-#{Sidekiq.server? ? "server" : "client"}-PID-#{$$}" if !options.has_key?(:id)
13
16
  options[:url] ||= determine_redis_provider
14
17
 
15
- size = options[:size] || (Sidekiq.server? ? (Sidekiq.options[:concurrency] + 5) : 5)
18
+ size = if options[:size]
19
+ options[:size]
20
+ elsif Sidekiq.server?
21
+ Sidekiq.options[:concurrency] + 5
22
+ elsif ENV['RAILS_MAX_THREADS']
23
+ Integer(ENV['RAILS_MAX_THREADS'])
24
+ else
25
+ 5
26
+ end
16
27
 
17
28
  verify_sizing(size, Sidekiq.options[:concurrency]) if Sidekiq.server?
18
29
 
@@ -35,7 +46,7 @@ module Sidekiq
35
46
  # - enterprise's leader election
36
47
  # - enterprise's cron support
37
48
  def verify_sizing(size, concurrency)
38
- raise ArgumentError, "Your Redis connection pool is too small for Sidekiq to work. Your pool has #{size} connections but really needs to have at least #{concurrency + 2}" if size <= concurrency
49
+ raise ArgumentError, "Your Redis connection pool is too small for Sidekiq to work. Your pool has #{size} connections but must have at least #{concurrency + 2}" if size <= concurrency
39
50
  end
40
51
 
41
52
  def build_client(options)
@@ -67,7 +78,14 @@ module Sidekiq
67
78
  opts.delete(:network_timeout)
68
79
  end
69
80
 
70
- opts[:driver] = opts[:driver] || 'ruby'
81
+ opts[:driver] ||= 'ruby'
82
+
83
+ # Issue #3303, redis-rb will silently retry an operation.
84
+ # This can lead to duplicate jobs if Sidekiq::Client's LPUSH
85
+ # is performed twice but I believe this is much, much rarer
86
+ # than the reconnect silently fixing a problem; we keep it
87
+ # on by default.
88
+ opts[:reconnect_attempts] ||= 1
71
89
 
72
90
  opts
73
91
  end
@@ -91,7 +109,15 @@ module Sidekiq
91
109
  end
92
110
 
93
111
  def determine_redis_provider
94
- ENV[ENV['REDIS_PROVIDER'] || 'REDIS_URL']
112
+ # If you have this in your environment:
113
+ # MY_REDIS_URL=redis://hostname.example.com:1238/4
114
+ # then set:
115
+ # REDIS_PROVIDER=MY_REDIS_URL
116
+ # and Sidekiq will find your custom URL variable with no custom
117
+ # initialization code at all.
118
+ ENV[
119
+ ENV['REDIS_PROVIDER'] || 'REDIS_URL'
120
+ ]
95
121
  end
96
122
 
97
123
  end
@@ -17,7 +17,7 @@ module Sidekiq
17
17
  # We need to go through the list one at a time to reduce the risk of something
18
18
  # going wrong between the time jobs are popped from the scheduled queue and when
19
19
  # they are pushed onto a work queue and losing the jobs.
20
- while job = conn.zrangebyscore(sorted_set, '-inf'.freeze, now, :limit => [0, 1]).first do
20
+ while job = conn.zrangebyscore(sorted_set, '-inf', now, :limit => [0, 1]).first do
21
21
 
22
22
  # Pop item off the queue and add it to the work queue. If the job can't be popped from
23
23
  # the queue, it's because another process already popped it so we can move on to the
@@ -79,9 +79,7 @@ module Sidekiq
79
79
  # Most likely a problem with redis networking.
80
80
  # Punt and try again at the next interval
81
81
  logger.error ex.message
82
- ex.backtrace.each do |bt|
83
- logger.error(bt)
84
- end
82
+ handle_exception(ex)
85
83
  end
86
84
  end
87
85
 
@@ -95,13 +93,38 @@ module Sidekiq
95
93
  # if poll_interval_average hasn't been calculated yet, we can
96
94
  # raise an error trying to reach Redis.
97
95
  logger.error ex.message
98
- logger.error ex.backtrace.first
96
+ handle_exception(ex)
99
97
  sleep 5
100
98
  end
101
99
 
102
- # Calculates a random interval that is ±50% the desired average.
103
100
  def random_poll_interval
104
- poll_interval_average * rand + poll_interval_average.to_f / 2
101
+ # We want one Sidekiq process to schedule jobs every N seconds. We have M processes
102
+ # and **don't** want to coordinate.
103
+ #
104
+ # So in N*M second timespan, we want each process to schedule once. The basic loop is:
105
+ #
106
+ # * sleep a random amount within that N*M timespan
107
+ # * wake up and schedule
108
+ #
109
+ # We want to avoid one edge case: imagine a set of 2 processes, scheduling every 5 seconds,
110
+ # so N*M = 10. Each process decides to randomly sleep 8 seconds, now we've failed to meet
111
+ # that 5 second average. Thankfully each schedule cycle will sleep randomly so the next
112
+ # iteration could see each process sleep for 1 second, undercutting our average.
113
+ #
114
+ # So below 10 processes, we special case and ensure the processes sleep closer to the average.
115
+ # In the example above, each process should schedule every 10 seconds on average. We special
116
+ # case smaller clusters to add 50% so they would sleep somewhere between 5 and 15 seconds.
117
+ # As we run more processes, the scheduling interval average will approach an even spread
118
+ # between 0 and poll interval so we don't need this artifical boost.
119
+ #
120
+ if process_count < 10
121
+ # For small clusters, calculate a random interval that is ±50% the desired average.
122
+ poll_interval_average * rand + poll_interval_average.to_f / 2
123
+ else
124
+ # With 10+ processes, we should have enough randomness to get decent polling
125
+ # across the entire timespan
126
+ poll_interval_average * rand
127
+ end
105
128
  end
106
129
 
107
130
  # We do our best to tune the poll interval to the size of the active Sidekiq
@@ -125,9 +148,13 @@ module Sidekiq
125
148
  # This minimizes a single point of failure by dispersing check-ins but without taxing
126
149
  # Redis if you run many Sidekiq processes.
127
150
  def scaled_poll_interval
151
+ process_count * Sidekiq.options[:average_scheduled_poll_interval]
152
+ end
153
+
154
+ def process_count
128
155
  pcount = Sidekiq::ProcessSet.new.size
129
156
  pcount = 1 if pcount == 0
130
- pcount * Sidekiq.options[:average_scheduled_poll_interval]
157
+ pcount
131
158
  end
132
159
 
133
160
  def initial_wait
@@ -55,6 +55,15 @@ module Sidekiq
55
55
  yield @server_chain if block_given?
56
56
  @server_chain
57
57
  end
58
+
59
+ def constantize(str)
60
+ names = str.split('::')
61
+ names.shift if names.empty? || names.first.empty?
62
+
63
+ names.inject(Object) do |constant, name|
64
+ constant.const_defined?(name) ? constant.const_get(name) : constant.const_missing(name)
65
+ end
66
+ end
58
67
  end
59
68
  end
60
69
 
@@ -63,29 +72,31 @@ module Sidekiq
63
72
 
64
73
  class EmptyQueueError < RuntimeError; end
65
74
 
66
- class Client
67
- alias_method :raw_push_real, :raw_push
68
-
75
+ module TestingClient
69
76
  def raw_push(payloads)
70
77
  if Sidekiq::Testing.fake?
71
78
  payloads.each do |job|
72
- Queues.push(job['queue'], job['class'], Sidekiq.load_json(Sidekiq.dump_json(job)))
79
+ job = Sidekiq.load_json(Sidekiq.dump_json(job))
80
+ job.merge!('enqueued_at' => Time.now.to_f) unless job['at']
81
+ Queues.push(job['queue'], job['class'], job)
73
82
  end
74
83
  true
75
84
  elsif Sidekiq::Testing.inline?
76
85
  payloads.each do |job|
77
- klass = job['class'].constantize
86
+ klass = Sidekiq::Testing.constantize(job['class'])
78
87
  job['id'] ||= SecureRandom.hex(12)
79
88
  job_hash = Sidekiq.load_json(Sidekiq.dump_json(job))
80
89
  klass.process_job(job_hash)
81
90
  end
82
91
  true
83
92
  else
84
- raw_push_real(payloads)
93
+ super
85
94
  end
86
95
  end
87
96
  end
88
97
 
98
+ Sidekiq::Client.prepend TestingClient
99
+
89
100
  module Queues
90
101
  ##
91
102
  # The Queues class is only for testing the fake queue implementation.
@@ -307,10 +318,16 @@ module Sidekiq
307
318
  worker_classes = jobs.map { |job| job["class"] }.uniq
308
319
 
309
320
  worker_classes.each do |worker_class|
310
- worker_class.constantize.drain
321
+ Sidekiq::Testing.constantize(worker_class).drain
311
322
  end
312
323
  end
313
324
  end
314
325
  end
315
326
  end
316
327
  end
328
+
329
+ if defined?(::Rails) && Rails.respond_to?(:env) && !Rails.env.test?
330
+ puts("**************************************************")
331
+ puts("⛔️ WARNING: Sidekiq testing API enabled, but this is not the test environment. Your jobs will not go to Redis.")
332
+ puts("**************************************************")
333
+ end
data/lib/sidekiq/util.rb CHANGED
@@ -2,7 +2,6 @@
2
2
  require 'socket'
3
3
  require 'securerandom'
4
4
  require 'sidekiq/exception_handler'
5
- require 'sidekiq/core_ext'
6
5
 
7
6
  module Sidekiq
8
7
  ##
@@ -22,6 +21,7 @@ module Sidekiq
22
21
 
23
22
  def safe_thread(name, &block)
24
23
  Thread.new do
24
+ Thread.current['sidekiq_label'] = name
25
25
  watchdog(name, &block)
26
26
  end
27
27
  end
@@ -46,7 +46,10 @@ module Sidekiq
46
46
  @@identity ||= "#{hostname}:#{$$}:#{process_nonce}"
47
47
  end
48
48
 
49
- def fire_event(event, reverse=false)
49
+ def fire_event(event, options={})
50
+ reverse = options[:reverse]
51
+ reraise = options[:reraise]
52
+
50
53
  arr = Sidekiq.options[:lifecycle_events][event]
51
54
  arr.reverse! if reverse
52
55
  arr.each do |block|
@@ -54,6 +57,7 @@ module Sidekiq
54
57
  block.call
55
58
  rescue => ex
56
59
  handle_exception(ex, { context: "Exception during Sidekiq lifecycle event.", event: event })
60
+ raise ex if reraise
57
61
  end
58
62
  end
59
63
  arr.clear
@@ -1,4 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
  module Sidekiq
3
- VERSION = "4.2.4"
3
+ VERSION = "5.2.0"
4
4
  end
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Sidekiq
4
4
  class WebAction
5
- RACK_SESSION = 'rack.session'.freeze
5
+ RACK_SESSION = 'rack.session'
6
6
 
7
7
  attr_accessor :env, :block, :type
8
8
 
@@ -39,10 +39,6 @@ module Sidekiq
39
39
  env[RACK_SESSION]
40
40
  end
41
41
 
42
- def content_type(type)
43
- @type = type
44
- end
45
-
46
42
  def erb(content, options = {})
47
43
  if content.kind_of? Symbol
48
44
  unless respond_to?(:"_erb_#{content}")
@@ -81,7 +77,7 @@ module Sidekiq
81
77
  private
82
78
 
83
79
  def _erb(file, locals)
84
- locals.each {|k, v| define_singleton_method(k){ v } } if locals
80
+ locals.each {|k, v| define_singleton_method(k){ v } unless (singleton_methods.include? k)} if locals
85
81
 
86
82
  if file.kind_of?(String)
87
83
  ERB.new(file).result(binding)
@@ -4,9 +4,24 @@ module Sidekiq
4
4
  class WebApplication
5
5
  extend WebRouter
6
6
 
7
- CONTENT_LENGTH = "Content-Length".freeze
8
- CONTENT_TYPE = "Content-Type".freeze
7
+ CONTENT_LENGTH = "Content-Length"
8
+ CONTENT_TYPE = "Content-Type"
9
9
  REDIS_KEYS = %w(redis_version uptime_in_days connected_clients used_memory_human used_memory_peak_human)
10
+ CSP_HEADER = [
11
+ "default-src 'self' https: http:",
12
+ "child-src 'self'",
13
+ "connect-src 'self' https: http: wss: ws:",
14
+ "font-src 'self' https: http:",
15
+ "frame-src 'self'",
16
+ "img-src 'self' https: http: data:",
17
+ "manifest-src 'self'",
18
+ "media-src 'self'",
19
+ "object-src 'none'",
20
+ "script-src 'self' https: http:",
21
+ "style-src 'self' https: http: 'unsafe-inline'",
22
+ "worker-src 'self'",
23
+ "base-uri 'self'"
24
+ ].join('; ').freeze
10
25
 
11
26
  def initialize(klass)
12
27
  @klass = klass
@@ -28,10 +43,6 @@ module Sidekiq
28
43
  # nothing, backwards compatibility
29
44
  end
30
45
 
31
- get "" do
32
- redirect(root_path)
33
- end
34
-
35
46
  get "/" do
36
47
  @redis_info = redis_info.select{ |k, v| REDIS_KEYS.include? k }
37
48
  stats_history = Sidekiq::Stats::History.new((params['days'] || 30).to_i)
@@ -89,7 +100,7 @@ module Sidekiq
89
100
  name = route_params[:name]
90
101
  Sidekiq::Job.new(params['key_val'], name).delete
91
102
 
92
- redirect_with_query("#{root_path}queues/#{name}")
103
+ redirect_with_query("#{root_path}queues/#{CGI.escape(name)}")
93
104
  end
94
105
 
95
106
  get '/morgue' do
@@ -238,7 +249,6 @@ module Sidekiq
238
249
  get '/stats' do
239
250
  sidekiq_stats = Sidekiq::Stats.new
240
251
  redis_stats = redis_info.select { |k, v| REDIS_KEYS.include? k }
241
-
242
252
  json(
243
253
  sidekiq: {
244
254
  processed: sidekiq_stats.processed,
@@ -251,7 +261,8 @@ module Sidekiq
251
261
  dead: sidekiq_stats.dead_size,
252
262
  default_latency: sidekiq_stats.default_queue_latency
253
263
  },
254
- redis: redis_stats
264
+ redis: redis_stats,
265
+ server_utc_time: server_utc_time
255
266
  )
256
267
  end
257
268
 
@@ -278,19 +289,15 @@ module Sidekiq
278
289
  resp = case resp
279
290
  when Array
280
291
  resp
281
- when Fixnum
282
- [resp, {}, []]
283
292
  else
284
- type_header = case action.type
285
- when :json
286
- { "Content-Type" => "application/json", "Cache-Control" => "no-cache" }
287
- when String
288
- { "Content-Type" => action.type, "Cache-Control" => "no-cache" }
289
- else
290
- { "Content-Type" => "text/html", "Cache-Control" => "no-cache" }
291
- end
292
-
293
- [200, type_header, [resp]]
293
+ headers = {
294
+ "Content-Type" => "text/html",
295
+ "Cache-Control" => "no-cache",
296
+ "Content-Language" => action.locale,
297
+ "Content-Security-Policy" => CSP_HEADER
298
+ }
299
+
300
+ [200, headers, [resp]]
294
301
  end
295
302
 
296
303
  resp[1] = resp[1].dup
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
  require 'uri'
3
+ require 'set'
3
4
  require 'yaml'
5
+ require 'cgi'
4
6
 
5
7
  module Sidekiq
6
8
  # This is not a public API
@@ -13,7 +15,7 @@ module Sidekiq
13
15
  settings.locales.each_with_object({}) do |path, global|
14
16
  find_locale_files(lang).each do |file|
15
17
  strs = YAML.load(File.open(file))
16
- global.deep_merge!(strs[lang])
18
+ global.merge!(strs[lang])
17
19
  end
18
20
  end
19
21
  end
@@ -22,6 +24,7 @@ module Sidekiq
22
24
  def clear_caches
23
25
  @@strings = nil
24
26
  @@locale_files = nil
27
+ @@available_locales = nil
25
28
  end
26
29
 
27
30
  def locale_files
@@ -30,6 +33,10 @@ module Sidekiq
30
33
  end
31
34
  end
32
35
 
36
+ def available_locales
37
+ @@available_locales ||= locale_files.map { |path| File.basename(path, '.yml') }.uniq
38
+ end
39
+
33
40
  def find_locale_files(lang)
34
41
  locale_files.select { |file| file =~ /\/#{lang}\.yml$/ }
35
42
  end
@@ -63,23 +70,52 @@ module Sidekiq
63
70
  end
64
71
  end
65
72
 
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
73
+ def text_direction
74
+ get_locale['TextDirection'] || 'ltr'
75
+ end
76
+
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 do |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
+ end.sort do |(_, left), (_, right)|
90
+ right <=> left
91
+ end.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 do |preferred|
101
+ preferred_language = preferred.split('-', 2).first
102
+
103
+ lang_group = available_locales.select do |available|
104
+ preferred_language == available.split('-', 2).first
105
+ end
106
+
107
+ lang_group.find { |lang| lang == preferred } || lang_group.min_by(&:length)
108
+ end.compact.first
109
+
110
+ matched_locale || 'en'
80
111
  end
81
112
  end
82
113
 
114
+ # mperham/sidekiq#3243
115
+ def unfiltered?
116
+ yield unless env['PATH_INFO'].start_with?("/filter/")
117
+ end
118
+
83
119
  def get_locale
84
120
  strings(locale)
85
121
  end
@@ -111,16 +147,15 @@ module Sidekiq
111
147
  end.map { |msg| Sidekiq.load_json(msg) }
112
148
  end
113
149
 
114
- def location
115
- Sidekiq.redis { |conn| conn.client.location }
116
- end
117
-
118
150
  def redis_connection
119
- Sidekiq.redis { |conn| conn.client.id }
151
+ Sidekiq.redis do |conn|
152
+ c = conn.connection
153
+ "redis://#{c[:location]}/#{c[:db]}"
154
+ end
120
155
  end
121
156
 
122
157
  def namespace
123
- @@ns ||= Sidekiq.redis { |conn| conn.respond_to?(:namespace) ? conn.namespace : nil }
158
+ @ns ||= Sidekiq.redis { |conn| conn.respond_to?(:namespace) ? conn.namespace : nil }
124
159
  end
125
160
 
126
161
  def redis_info
@@ -140,7 +175,8 @@ module Sidekiq
140
175
  end
141
176
 
142
177
  def relative_time(time)
143
- %{<time datetime="#{time.getutc.iso8601}">#{time}</time>}
178
+ stamp = time.getutc.iso8601
179
+ %{<time class="ltr" dir="ltr" title="#{stamp}" datetime="#{stamp}">#{time}</time>}
144
180
  end
145
181
 
146
182
  def job_params(job, score)
@@ -148,7 +184,7 @@ module Sidekiq
148
184
  end
149
185
 
150
186
  def parse_params(params)
151
- score, jid = params.split("-")
187
+ score, jid = params.split("-", 2)
152
188
  [score.to_f, jid]
153
189
  end
154
190
 
@@ -156,9 +192,13 @@ module Sidekiq
156
192
 
157
193
  # Merge options with current params, filter safe params, and stringify to query string
158
194
  def qparams(options)
159
- options = options.stringify_keys
195
+ # stringify
196
+ options.keys.each do |key|
197
+ options[key.to_s] = options.delete(key)
198
+ end
199
+
160
200
  params.merge(options).map do |key, value|
161
- SAFE_QPARAMS.include?(key) ? "#{key}=#{value}" : next
201
+ SAFE_QPARAMS.include?(key) ? "#{key}=#{CGI.escape(value.to_s)}" : next
162
202
  end.compact.join("&")
163
203
  end
164
204
 
@@ -246,6 +286,10 @@ module Sidekiq
246
286
  "Sidekiq v#{Sidekiq::VERSION}"
247
287
  end
248
288
 
289
+ def server_utc_time
290
+ Time.now.utc.strftime('%H:%M:%S UTC')
291
+ end
292
+
249
293
  def redis_connection_and_namespace
250
294
  @redis_connection_and_namespace ||= begin
251
295
  namespace_suffix = namespace == nil ? '' : "##{namespace}"
@@ -3,16 +3,16 @@ require 'rack'
3
3
 
4
4
  module Sidekiq
5
5
  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
6
+ GET = 'GET'
7
+ DELETE = 'DELETE'
8
+ POST = 'POST'
9
+ PUT = 'PUT'
10
+ PATCH = 'PATCH'
11
+ HEAD = 'HEAD'
12
12
 
13
- ROUTE_PARAMS = 'rack.route_params'.freeze
14
- REQUEST_METHOD = 'REQUEST_METHOD'.freeze
15
- PATH_INFO = 'PATH_INFO'.freeze
13
+ ROUTE_PARAMS = 'rack.route_params'
14
+ REQUEST_METHOD = 'REQUEST_METHOD'
15
+ PATH_INFO = 'PATH_INFO'
16
16
 
17
17
  def get(path, &block)
18
18
  route(GET, path, &block)
@@ -45,6 +45,10 @@ module Sidekiq
45
45
  request_method = env[REQUEST_METHOD]
46
46
  path_info = ::Rack::Utils.unescape env[PATH_INFO]
47
47
 
48
+ # There are servers which send an empty string when requesting the root.
49
+ # These servers should be ashamed of themselves.
50
+ path_info = "/" if path_info == ""
51
+
48
52
  @routes[request_method].each do |route|
49
53
  if params = route.match(request_method, path_info)
50
54
  env[ROUTE_PARAMS] = params
@@ -60,7 +64,7 @@ module Sidekiq
60
64
  class WebRoute
61
65
  attr_accessor :request_method, :pattern, :block, :name
62
66
 
63
- NAMED_SEGMENTS_PATTERN = /\/([^\/]*):([^\.:$\/]+)/.freeze
67
+ NAMED_SEGMENTS_PATTERN = /\/([^\/]*):([^\.:$\/]+)/
64
68
 
65
69
  def initialize(request_method, pattern, block)
66
70
  @request_method = request_method