sidekiq 7.3.0 → 8.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. checksums.yaml +4 -4
  2. data/Changes.md +158 -0
  3. data/README.md +16 -13
  4. data/bin/sidekiqload +31 -22
  5. data/bin/webload +69 -0
  6. data/lib/active_job/queue_adapters/sidekiq_adapter.rb +120 -0
  7. data/lib/generators/sidekiq/job_generator.rb +2 -0
  8. data/lib/sidekiq/api.rb +184 -71
  9. data/lib/sidekiq/capsule.rb +11 -9
  10. data/lib/sidekiq/cli.rb +16 -20
  11. data/lib/sidekiq/client.rb +28 -11
  12. data/lib/sidekiq/component.rb +62 -2
  13. data/lib/sidekiq/config.rb +42 -18
  14. data/lib/sidekiq/deploy.rb +2 -0
  15. data/lib/sidekiq/embedded.rb +4 -1
  16. data/lib/sidekiq/iterable_job.rb +3 -0
  17. data/lib/sidekiq/job/interrupt_handler.rb +2 -0
  18. data/lib/sidekiq/job/iterable/active_record_enumerator.rb +3 -3
  19. data/lib/sidekiq/job/iterable.rb +82 -7
  20. data/lib/sidekiq/job_logger.rb +15 -27
  21. data/lib/sidekiq/job_retry.rb +17 -5
  22. data/lib/sidekiq/job_util.rb +7 -1
  23. data/lib/sidekiq/launcher.rb +3 -2
  24. data/lib/sidekiq/logger.rb +19 -70
  25. data/lib/sidekiq/manager.rb +0 -1
  26. data/lib/sidekiq/metrics/query.rb +73 -45
  27. data/lib/sidekiq/metrics/shared.rb +23 -9
  28. data/lib/sidekiq/metrics/tracking.rb +22 -12
  29. data/lib/sidekiq/middleware/current_attributes.rb +12 -4
  30. data/lib/sidekiq/middleware/modules.rb +2 -0
  31. data/lib/sidekiq/monitor.rb +2 -1
  32. data/lib/sidekiq/paginator.rb +14 -1
  33. data/lib/sidekiq/processor.rb +26 -19
  34. data/lib/sidekiq/profiler.rb +72 -0
  35. data/lib/sidekiq/rails.rb +44 -55
  36. data/lib/sidekiq/redis_client_adapter.rb +0 -1
  37. data/lib/sidekiq/redis_connection.rb +22 -4
  38. data/lib/sidekiq/ring_buffer.rb +2 -0
  39. data/lib/sidekiq/systemd.rb +2 -0
  40. data/lib/sidekiq/testing.rb +7 -7
  41. data/lib/sidekiq/version.rb +6 -2
  42. data/lib/sidekiq/web/action.rb +124 -69
  43. data/lib/sidekiq/web/application.rb +355 -377
  44. data/lib/sidekiq/web/config.rb +120 -0
  45. data/lib/sidekiq/web/helpers.rb +64 -33
  46. data/lib/sidekiq/web/router.rb +61 -74
  47. data/lib/sidekiq/web.rb +52 -150
  48. data/lib/sidekiq.rb +5 -4
  49. data/sidekiq.gemspec +6 -6
  50. data/web/assets/javascripts/application.js +6 -13
  51. data/web/assets/javascripts/base-charts.js +30 -16
  52. data/web/assets/javascripts/chartjs-adapter-date-fns.min.js +7 -0
  53. data/web/assets/javascripts/dashboard-charts.js +2 -0
  54. data/web/assets/javascripts/dashboard.js +7 -1
  55. data/web/assets/javascripts/metrics.js +16 -34
  56. data/web/assets/stylesheets/style.css +766 -0
  57. data/web/locales/ar.yml +1 -0
  58. data/web/locales/cs.yml +1 -0
  59. data/web/locales/da.yml +1 -0
  60. data/web/locales/de.yml +1 -0
  61. data/web/locales/el.yml +1 -0
  62. data/web/locales/en.yml +9 -1
  63. data/web/locales/es.yml +24 -2
  64. data/web/locales/fa.yml +1 -0
  65. data/web/locales/fr.yml +1 -1
  66. data/web/locales/gd.yml +1 -1
  67. data/web/locales/he.yml +1 -0
  68. data/web/locales/hi.yml +1 -0
  69. data/web/locales/it.yml +40 -1
  70. data/web/locales/ja.yml +1 -1
  71. data/web/locales/ko.yml +1 -0
  72. data/web/locales/lt.yml +1 -0
  73. data/web/locales/nb.yml +1 -0
  74. data/web/locales/nl.yml +1 -0
  75. data/web/locales/pl.yml +1 -0
  76. data/web/locales/{pt-br.yml → pt-BR.yml} +3 -3
  77. data/web/locales/pt.yml +1 -0
  78. data/web/locales/ru.yml +1 -0
  79. data/web/locales/sv.yml +1 -0
  80. data/web/locales/ta.yml +1 -0
  81. data/web/locales/tr.yml +2 -2
  82. data/web/locales/uk.yml +25 -1
  83. data/web/locales/ur.yml +1 -0
  84. data/web/locales/vi.yml +1 -0
  85. data/web/locales/{zh-cn.yml → zh-CN.yml} +85 -74
  86. data/web/locales/{zh-tw.yml → zh-TW.yml} +2 -2
  87. data/web/views/_footer.erb +31 -34
  88. data/web/views/_job_info.erb +91 -89
  89. data/web/views/_metrics_period_select.erb +13 -10
  90. data/web/views/_nav.erb +14 -21
  91. data/web/views/_paging.erb +23 -21
  92. data/web/views/_poll_link.erb +2 -2
  93. data/web/views/_summary.erb +16 -16
  94. data/web/views/busy.erb +124 -122
  95. data/web/views/dashboard.erb +63 -64
  96. data/web/views/dead.erb +31 -27
  97. data/web/views/filtering.erb +3 -4
  98. data/web/views/layout.erb +13 -29
  99. data/web/views/metrics.erb +75 -82
  100. data/web/views/metrics_for_job.erb +45 -46
  101. data/web/views/morgue.erb +61 -70
  102. data/web/views/profiles.erb +43 -0
  103. data/web/views/queue.erb +54 -52
  104. data/web/views/queues.erb +43 -41
  105. data/web/views/retries.erb +66 -75
  106. data/web/views/retry.erb +32 -27
  107. data/web/views/scheduled.erb +59 -55
  108. data/web/views/scheduled_job_info.erb +1 -1
  109. metadata +27 -29
  110. data/web/assets/stylesheets/application-dark.css +0 -147
  111. data/web/assets/stylesheets/application-rtl.css +0 -163
  112. data/web/assets/stylesheets/application.css +0 -758
  113. data/web/assets/stylesheets/bootstrap-rtl.min.css +0 -9
  114. data/web/assets/stylesheets/bootstrap.css +0 -5
  115. data/web/views/_status.erb +0 -4
data/lib/sidekiq/rails.rb CHANGED
@@ -1,72 +1,61 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "sidekiq/job"
4
- require "rails"
5
-
6
3
  module Sidekiq
7
- class Rails < ::Rails::Engine
8
- class Reloader
9
- def initialize(app = ::Rails.application)
10
- @app = app
11
- end
12
-
13
- def call
14
- params = (::Rails::VERSION::STRING >= "7.1") ? {source: "job.sidekiq"} : {}
15
- @app.reloader.wrap(**params) do
16
- yield
4
+ begin
5
+ gem "railties", ">= 7.0"
6
+ require "rails"
7
+ require "sidekiq/job"
8
+ require_relative "../active_job/queue_adapters/sidekiq_adapter"
9
+
10
+ class Rails < ::Rails::Engine
11
+ class Reloader
12
+ def initialize(app = ::Rails.application)
13
+ @app = app
17
14
  end
18
- end
19
15
 
20
- def inspect
21
- "#<Sidekiq::Rails::Reloader @app=#{@app.class.name}>"
22
- end
16
+ def call
17
+ params = (::Rails::VERSION::STRING >= "7.1") ? {source: "job.sidekiq"} : {}
18
+ @app.reloader.wrap(**params) do
19
+ yield
20
+ end
21
+ end
23
22
 
24
- def to_hash
25
- {app: @app.class.name}
26
- end
27
- end
23
+ def inspect
24
+ "#<Sidekiq::Rails::Reloader @app=#{@app.class.name}>"
25
+ end
28
26
 
29
- # By including the Options module, we allow AJs to directly control sidekiq features
30
- # via the *sidekiq_options* class method and, for instance, not use AJ's retry system.
31
- # AJ retries don't show up in the Sidekiq UI Retries tab, don't save any error data, can't be
32
- # manually retried, don't automatically die, etc.
33
- #
34
- # class SomeJob < ActiveJob::Base
35
- # queue_as :default
36
- # sidekiq_options retry: 3, backtrace: 10
37
- # def perform
38
- # end
39
- # end
40
- initializer "sidekiq.active_job_integration" do
41
- ActiveSupport.on_load(:active_job) do
42
- include ::Sidekiq::Job::Options unless respond_to?(:sidekiq_options)
27
+ def to_hash
28
+ {app: @app.class.name}
29
+ end
43
30
  end
44
- end
45
31
 
46
- initializer "sidekiq.backtrace_cleaner" do
47
- Sidekiq.configure_server do |config|
48
- config[:backtrace_cleaner] = ->(backtrace) { ::Rails.backtrace_cleaner.clean(backtrace) }
32
+ initializer "sidekiq.backtrace_cleaner" do
33
+ Sidekiq.configure_server do |config|
34
+ config[:backtrace_cleaner] = ->(backtrace) { ::Rails.backtrace_cleaner.clean(backtrace) }
35
+ end
49
36
  end
50
- end
51
-
52
- # This hook happens after all initializers are run, just before returning
53
- # from config/environment.rb back to sidekiq/cli.rb.
54
- #
55
- # None of this matters on the client-side, only within the Sidekiq process itself.
56
- config.after_initialize do
57
- Sidekiq.configure_server do |config|
58
- config[:reloader] = Sidekiq::Rails::Reloader.new
59
37
 
60
- # This is the integration code necessary so that if a job uses `Rails.logger.info "Hello"`,
61
- # it will appear in the Sidekiq console with all of the job context.
62
- unless ::Rails.logger == config.logger || ::ActiveSupport::Logger.logger_outputs_to?(::Rails.logger, $stdout)
63
- if ::Rails.logger.respond_to?(:broadcast_to)
64
- ::Rails.logger.broadcast_to(config.logger)
65
- else
66
- ::Rails.logger.extend(::ActiveSupport::Logger.broadcast(config.logger))
38
+ # This hook happens after all initializers are run, just before returning
39
+ # from config/environment.rb back to sidekiq/cli.rb.
40
+ #
41
+ # None of this matters on the client-side, only within the Sidekiq process itself.
42
+ config.after_initialize do
43
+ Sidekiq.configure_server do |config|
44
+ config[:reloader] = Sidekiq::Rails::Reloader.new
45
+
46
+ # This is the integration code necessary so that if a job uses `Rails.logger.info "Hello"`,
47
+ # it will appear in the Sidekiq console with all of the job context.
48
+ unless ::Rails.logger == config.logger || ::ActiveSupport::Logger.logger_outputs_to?(::Rails.logger, $stdout)
49
+ if ::Rails.logger.respond_to?(:broadcast_to)
50
+ ::Rails.logger.broadcast_to(config.logger)
51
+ else
52
+ ::Rails.logger.extend(::ActiveSupport::Logger.broadcast(config.logger))
53
+ end
67
54
  end
68
55
  end
69
56
  end
70
57
  end
58
+ rescue Gem::LoadError
59
+ # Rails not available or version requirement not met
71
60
  end
72
61
  end
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "set"
4
3
  require "redis_client"
5
4
  require "redis_client/decorator"
6
5
 
@@ -10,6 +10,8 @@ module Sidekiq
10
10
  def create(options = {})
11
11
  symbolized_options = deep_symbolize_keys(options)
12
12
  symbolized_options[:url] ||= determine_redis_provider
13
+ symbolized_options[:password] = wrap(symbolized_options[:password]) if symbolized_options.key?(:password)
14
+ symbolized_options[:sentinel_password] = wrap(symbolized_options[:sentinel_password]) if symbolized_options.key?(:sentinel_password)
13
15
 
14
16
  logger = symbolized_options.delete(:logger)
15
17
  logger&.info { "Sidekiq #{Sidekiq::VERSION} connecting to Redis with options #{scrub(symbolized_options)}" }
@@ -38,6 +40,15 @@ module Sidekiq
38
40
 
39
41
  private
40
42
 
43
+ # Wrap hard-coded passwords in a Proc to avoid logging the value
44
+ def wrap(pwd)
45
+ if pwd.is_a?(String)
46
+ ->(username) { pwd }
47
+ else
48
+ pwd
49
+ end
50
+ end
51
+
41
52
  def deep_symbolize_keys(object)
42
53
  case object
43
54
  when Hash
@@ -57,16 +68,23 @@ module Sidekiq
57
68
  # Deep clone so we can muck with these options all we want and exclude
58
69
  # params from dump-and-load that may contain objects that Marshal is
59
70
  # unable to safely dump.
60
- keys = options.keys - [:logger, :ssl_params]
71
+ keys = options.keys - [:logger, :ssl_params, :password, :sentinel_password]
61
72
  scrubbed_options = Marshal.load(Marshal.dump(options.slice(*keys)))
62
73
  if scrubbed_options[:url] && (uri = URI.parse(scrubbed_options[:url])) && uri.password
63
74
  uri.password = redacted
64
75
  scrubbed_options[:url] = uri.to_s
65
76
  end
66
- scrubbed_options[:password] = redacted if scrubbed_options[:password]
67
- scrubbed_options[:sentinel_password] = redacted if scrubbed_options[:sentinel_password]
77
+ scrubbed_options[:password] = redacted if options.key?(:password)
78
+ scrubbed_options[:sentinel_password] = redacted if options.key?(:sentinel_password)
68
79
  scrubbed_options[:sentinels]&.each do |sentinel|
69
- sentinel[:password] = redacted if sentinel[:password]
80
+ if sentinel.is_a?(String)
81
+ if (uri = URI(sentinel)) && uri.password
82
+ uri.password = redacted
83
+ sentinel.replace(uri.to_s)
84
+ end
85
+ elsif sentinel[:password]
86
+ sentinel[:password] = redacted
87
+ end
70
88
  end
71
89
  scrubbed_options
72
90
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "forwardable"
2
4
 
3
5
  module Sidekiq
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  #
2
4
  # Sidekiq's systemd integration allows Sidekiq to inform systemd:
3
5
  # 1. when it has successfully started
@@ -87,7 +87,7 @@ module Sidekiq
87
87
  if Sidekiq::Testing.fake?
88
88
  payloads.each do |job|
89
89
  job = Sidekiq.load_json(Sidekiq.dump_json(job))
90
- job["enqueued_at"] = Time.now.to_f unless job["at"]
90
+ job["enqueued_at"] = ::Process.clock_gettime(::Process::CLOCK_REALTIME, :millisecond) unless job["at"]
91
91
  Queues.push(job["queue"], job["class"], job)
92
92
  end
93
93
  true
@@ -283,11 +283,11 @@ module Sidekiq
283
283
  end
284
284
 
285
285
  def process_job(job)
286
- inst = new
287
- inst.jid = job["jid"]
288
- inst.bid = job["bid"] if inst.respond_to?(:bid=)
289
- Sidekiq::Testing.server_middleware.invoke(inst, job, job["queue"]) do
290
- execute_job(inst, job["args"])
286
+ instance = new
287
+ instance.jid = job["jid"]
288
+ instance.bid = job["bid"] if instance.respond_to?(:bid=)
289
+ Sidekiq::Testing.server_middleware.invoke(instance, job, job["queue"]) do
290
+ execute_job(instance, job["args"])
291
291
  end
292
292
  end
293
293
 
@@ -329,6 +329,6 @@ module Sidekiq
329
329
  end
330
330
  end
331
331
 
332
- if defined?(::Rails) && Rails.respond_to?(:env) && !Rails.env.test? && !$TESTING
332
+ if defined?(::Rails) && Rails.respond_to?(:env) && !Rails.env.test? && !$TESTING # rubocop:disable Style/GlobalVars
333
333
  warn("⛔️ WARNING: Sidekiq testing API enabled, but this is not the test environment. Your jobs will not go to Redis.", uplevel: 1)
334
334
  end
@@ -1,6 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Sidekiq
4
- VERSION = "7.3.0"
5
- MAJOR = 7
4
+ VERSION = "8.0.5"
5
+ MAJOR = 8
6
+
7
+ def self.gem_version
8
+ Gem::Version.new(VERSION)
9
+ end
6
10
  end
@@ -1,99 +1,154 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "erb"
4
+
3
5
  module Sidekiq
4
- class WebAction
5
- RACK_SESSION = "rack.session"
6
+ class Web
7
+ ##
8
+ # These instance methods are available to all executing ERB
9
+ # templates.
10
+ class Action
11
+ attr_accessor :env, :block
12
+
13
+ def initialize(env, block)
14
+ @_erb = false
15
+ @env = env
16
+ @block = block
17
+ end
6
18
 
7
- attr_accessor :env, :block, :type
19
+ def config
20
+ env[:web_config]
21
+ end
8
22
 
9
- def settings
10
- Web.settings
11
- end
23
+ def request
24
+ @request ||= ::Rack::Request.new(env)
25
+ end
12
26
 
13
- def request
14
- @request ||= ::Rack::Request.new(env)
15
- end
27
+ def halt(res)
28
+ throw :halt, [res, {"content-type" => "text/plain"}, [res.to_s]]
29
+ end
16
30
 
17
- def halt(res)
18
- throw :halt, [res, {Rack::CONTENT_TYPE => "text/plain"}, [res.to_s]]
19
- end
31
+ # external redirect
32
+ def redirect_to(url)
33
+ throw :halt, [302, {"location" => url}, []]
34
+ end
20
35
 
21
- def redirect(location)
22
- throw :halt, [302, {Web::LOCATION => "#{request.base_url}#{location}"}, []]
23
- end
36
+ def header(key, value)
37
+ env["response_headers"][key] = value.to_s
38
+ end
24
39
 
25
- def reload_page
26
- current_location = request.referer.gsub(request.base_url, "")
27
- redirect current_location
28
- end
40
+ # internal redirect
41
+ def redirect(location)
42
+ throw :halt, [302, {"location" => "#{request.base_url}#{location}"}, []]
43
+ end
29
44
 
30
- def params
31
- indifferent_hash = Hash.new { |hash, key| hash[key.to_s] if Symbol === key }
45
+ def reload_page
46
+ current_location = request.referer.gsub(request.base_url, "")
47
+ redirect current_location
48
+ end
32
49
 
33
- indifferent_hash.merge! request.params
34
- route_params.each { |k, v| indifferent_hash[k.to_s] = v }
50
+ # stuff after ? or form input
51
+ # uses String keys, no Symbols!
52
+ def url_params(key)
53
+ warn { "URL parameter `#{key}` should be accessed via String, not Symbol (at #{caller(3..3).first})" } if key.is_a?(Symbol)
54
+ request.params[key.to_s]
55
+ end
35
56
 
36
- indifferent_hash
37
- end
57
+ # variables embedded in path, `/metrics/:name`
58
+ # uses Symbol keys, no Strings!
59
+ def route_params(key)
60
+ warn { "Route parameter `#{key}` should be accessed via Symbol, not String (at #{caller(3..3).first})" } if key.is_a?(String)
61
+ env["rack.route_params"][key.to_sym]
62
+ end
38
63
 
39
- def route_params
40
- env[WebRouter::ROUTE_PARAMS]
41
- end
64
+ def params
65
+ warn { "Direct access to Rack parameters is discouraged, use `url_params` or `route_params` (at #{caller(3..3).first})" }
66
+ request.params
67
+ end
42
68
 
43
- def session
44
- env[RACK_SESSION]
45
- end
69
+ def session
70
+ env["rack.session"]
71
+ end
46
72
 
47
- def erb(content, options = {})
48
- if content.is_a? Symbol
49
- unless respond_to?(:"_erb_#{content}")
50
- views = options[:views] || Web.settings.views
51
- src = ERB.new(File.read("#{views}/#{content}.erb")).src
52
- WebAction.class_eval <<-RUBY, __FILE__, __LINE__ + 1
53
- def _erb_#{content}
54
- #{src}
55
- end
56
- RUBY
57
- end
73
+ def logger
74
+ Sidekiq.logger
58
75
  end
59
76
 
60
- if @_erb
61
- _erb(content, options[:locals])
62
- else
63
- @_erb = true
64
- content = _erb(content, options[:locals])
77
+ # flash { "Some message to show on redirect" }
78
+ def flash
79
+ msg = yield
80
+ logger.info msg
81
+ session[:flash] = msg
82
+ end
65
83
 
66
- _render { content }
84
+ def flash?
85
+ session&.[](:flash)
67
86
  end
68
- end
69
87
 
70
- def render(engine, content, options = {})
71
- raise "Only erb templates are supported" if engine != :erb
88
+ def get_flash
89
+ @flash ||= session.delete(:flash)
90
+ end
72
91
 
73
- erb(content, options)
74
- end
92
+ def erb(content, options = {})
93
+ if content.is_a? Symbol
94
+ unless respond_to?(:"_erb_#{content}")
95
+ views = options[:views] || Web.views
96
+ filename = "#{views}/#{content}.erb"
97
+ src = ERB.new(File.read(filename)).src
98
+
99
+ # Need to use lineno less by 1 because erb generates a
100
+ # comment before the source code.
101
+ Action.class_eval <<-RUBY, filename, -1 # standard:disable Style/EvalWithLocation
102
+ def _erb_#{content}
103
+ #{src}
104
+ end
105
+ RUBY
106
+ end
107
+ end
75
108
 
76
- def json(payload)
77
- [200, {Rack::CONTENT_TYPE => "application/json", Rack::CACHE_CONTROL => "private, no-store"}, [Sidekiq.dump_json(payload)]]
78
- end
109
+ if @_erb
110
+ _erb(content, options[:locals])
111
+ else
112
+ @_erb = true
113
+ content = _erb(content, options[:locals])
79
114
 
80
- def initialize(env, block)
81
- @_erb = false
82
- @env = env
83
- @block = block
84
- @files ||= {}
85
- end
115
+ _render { content }
116
+ end
117
+ end
86
118
 
87
- private
119
+ def render(engine, content, options = {})
120
+ raise "Only erb templates are supported" if engine != :erb
88
121
 
89
- def _erb(file, locals)
90
- locals&.each { |k, v| define_singleton_method(k) { v } unless singleton_methods.include? k }
122
+ erb(content, options)
123
+ end
124
+
125
+ def json(payload)
126
+ [200,
127
+ {"content-type" => "application/json", "cache-control" => "private, no-store"},
128
+ [Sidekiq.dump_json(payload)]]
129
+ end
130
+
131
+ private
132
+
133
+ def warn
134
+ Sidekiq.logger.warn yield
135
+ end
136
+
137
+ def _erb(file, locals)
138
+ locals&.each { |k, v| define_singleton_method(k) { v } unless singleton_methods.include? k }
91
139
 
92
- if file.is_a?(String)
93
- ERB.new(file).result(binding)
94
- else
95
- send(:"_erb_#{file}")
140
+ if file.is_a?(String)
141
+ ERB.new(file).result(binding)
142
+ else
143
+ send(:"_erb_#{file}")
144
+ end
96
145
  end
146
+
147
+ class_eval <<-RUBY, ::Sidekiq::Web::LAYOUT, -1 # standard:disable Style/EvalWithLocation
148
+ def _render
149
+ #{ERB.new(File.read(::Sidekiq::Web::LAYOUT)).src}
150
+ end
151
+ RUBY
97
152
  end
98
153
  end
99
154
  end