sidekiq 7.3.9 → 8.1.7

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 (155) hide show
  1. checksums.yaml +4 -4
  2. data/Changes.md +225 -0
  3. data/README.md +34 -14
  4. data/bin/kiq +17 -0
  5. data/bin/lint-herb +13 -0
  6. data/bin/sidekiqload +12 -10
  7. data/bin/webload +69 -0
  8. data/lib/active_job/queue_adapters/sidekiq_adapter.rb +107 -58
  9. data/lib/generators/sidekiq/job_generator.rb +15 -3
  10. data/lib/generators/sidekiq/templates/job.rb.erb +1 -1
  11. data/lib/sidekiq/api.rb +310 -83
  12. data/lib/sidekiq/capsule.rb +10 -7
  13. data/lib/sidekiq/cli.rb +31 -24
  14. data/lib/sidekiq/client.rb +36 -18
  15. data/lib/sidekiq/component.rb +45 -3
  16. data/lib/sidekiq/config.rb +35 -30
  17. data/lib/sidekiq/embedded.rb +2 -1
  18. data/lib/sidekiq/fetch.rb +1 -0
  19. data/lib/sidekiq/iterable_job.rb +1 -0
  20. data/lib/sidekiq/job/iterable.rb +46 -18
  21. data/lib/sidekiq/job.rb +18 -3
  22. data/lib/sidekiq/job_logger.rb +8 -6
  23. data/lib/sidekiq/job_retry.rb +45 -18
  24. data/lib/sidekiq/job_util.rb +5 -1
  25. data/lib/sidekiq/launcher.rb +22 -12
  26. data/lib/sidekiq/loader.rb +57 -0
  27. data/lib/sidekiq/logger.rb +25 -69
  28. data/lib/sidekiq/manager.rb +2 -2
  29. data/lib/sidekiq/metrics/query.rb +77 -46
  30. data/lib/sidekiq/metrics/shared.rb +8 -5
  31. data/lib/sidekiq/metrics/tracking.rb +12 -7
  32. data/lib/sidekiq/middleware/current_attributes.rb +11 -19
  33. data/lib/sidekiq/middleware/i18n.rb +2 -0
  34. data/lib/sidekiq/monitor.rb +4 -8
  35. data/lib/sidekiq/paginator.rb +18 -2
  36. data/lib/sidekiq/processor.rb +26 -18
  37. data/lib/sidekiq/profiler.rb +73 -0
  38. data/lib/sidekiq/rails.rb +46 -67
  39. data/lib/sidekiq/redis_client_adapter.rb +6 -3
  40. data/lib/sidekiq/redis_connection.rb +16 -5
  41. data/lib/sidekiq/ring_buffer.rb +1 -0
  42. data/lib/sidekiq/scheduled.rb +6 -7
  43. data/lib/sidekiq/test_api.rb +331 -0
  44. data/lib/sidekiq/testing/inline.rb +2 -30
  45. data/lib/sidekiq/testing.rb +2 -333
  46. data/lib/sidekiq/transaction_aware_client.rb +13 -5
  47. data/lib/sidekiq/tui/controls.rb +53 -0
  48. data/lib/sidekiq/tui/filtering.rb +53 -0
  49. data/lib/sidekiq/tui/tabs/base_tab.rb +230 -0
  50. data/lib/sidekiq/tui/tabs/busy.rb +104 -0
  51. data/lib/sidekiq/tui/tabs/dead.rb +14 -0
  52. data/lib/sidekiq/tui/tabs/home.rb +115 -0
  53. data/lib/sidekiq/tui/tabs/metrics.rb +114 -0
  54. data/lib/sidekiq/tui/tabs/queues.rb +83 -0
  55. data/lib/sidekiq/tui/tabs/retries.rb +14 -0
  56. data/lib/sidekiq/tui/tabs/scheduled.rb +14 -0
  57. data/lib/sidekiq/tui/tabs/set_tab.rb +91 -0
  58. data/lib/sidekiq/tui/tabs.rb +15 -0
  59. data/lib/sidekiq/tui.rb +390 -0
  60. data/lib/sidekiq/version.rb +2 -2
  61. data/lib/sidekiq/web/action.rb +146 -83
  62. data/lib/sidekiq/web/application.rb +372 -328
  63. data/lib/sidekiq/web/config.rb +117 -0
  64. data/lib/sidekiq/web/helpers.rb +102 -44
  65. data/lib/sidekiq/web/router.rb +61 -77
  66. data/lib/sidekiq/web.rb +65 -151
  67. data/lib/sidekiq.rb +13 -1
  68. data/sidekiq.gemspec +9 -8
  69. data/web/assets/images/logo.png +0 -0
  70. data/web/assets/images/status.png +0 -0
  71. data/web/assets/javascripts/application.js +43 -27
  72. data/web/assets/javascripts/base-charts.js +30 -16
  73. data/web/assets/javascripts/chartjs-adapter-date-fns.min.js +7 -0
  74. data/web/assets/javascripts/dashboard.js +1 -1
  75. data/web/assets/javascripts/metrics.js +16 -34
  76. data/web/assets/stylesheets/style.css +776 -0
  77. data/web/locales/ar.yml +3 -1
  78. data/web/locales/cs.yml +2 -0
  79. data/web/locales/da.yml +2 -0
  80. data/web/locales/de.yml +2 -0
  81. data/web/locales/el.yml +2 -0
  82. data/web/locales/en.yml +8 -0
  83. data/web/locales/es.yml +25 -2
  84. data/web/locales/fa.yml +3 -1
  85. data/web/locales/fr.yml +3 -1
  86. data/web/locales/gd.yml +13 -1
  87. data/web/locales/he.yml +3 -1
  88. data/web/locales/hi.yml +36 -0
  89. data/web/locales/it.yml +9 -0
  90. data/web/locales/ja.yml +2 -0
  91. data/web/locales/ko.yml +2 -0
  92. data/web/locales/lt.yml +2 -0
  93. data/web/locales/nb.yml +2 -0
  94. data/web/locales/nl.yml +2 -0
  95. data/web/locales/pl.yml +2 -0
  96. data/web/locales/{pt-br.yml → pt-BR.yml} +3 -1
  97. data/web/locales/pt.yml +2 -0
  98. data/web/locales/ru.yml +2 -0
  99. data/web/locales/sv.yml +2 -0
  100. data/web/locales/ta.yml +83 -47
  101. data/web/locales/tr.yml +2 -0
  102. data/web/locales/uk.yml +7 -5
  103. data/web/locales/ur.yml +3 -1
  104. data/web/locales/vi.yml +2 -0
  105. data/web/locales/{zh-cn.yml → zh-CN.yml} +86 -73
  106. data/web/locales/{zh-tw.yml → zh-TW.yml} +3 -1
  107. data/web/views/_footer.html.erb +32 -0
  108. data/web/views/_job_info.html.erb +115 -0
  109. data/web/views/_metrics_period_select.html.erb +15 -0
  110. data/web/views/_nav.html.erb +45 -0
  111. data/web/views/_paging.html.erb +26 -0
  112. data/web/views/_poll_link.html.erb +4 -0
  113. data/web/views/{_summary.erb → _summary.html.erb} +16 -16
  114. data/web/views/busy.html.erb +155 -0
  115. data/web/views/dashboard.html.erb +104 -0
  116. data/web/views/dead.html.erb +38 -0
  117. data/web/views/filtering.html.erb +6 -0
  118. data/web/views/layout.html.erb +26 -0
  119. data/web/views/metrics.html.erb +85 -0
  120. data/web/views/metrics_for_job.html.erb +58 -0
  121. data/web/views/morgue.html.erb +69 -0
  122. data/web/views/profiles.html.erb +43 -0
  123. data/web/views/queue.html.erb +57 -0
  124. data/web/views/queues.html.erb +46 -0
  125. data/web/views/retries.html.erb +77 -0
  126. data/web/views/retry.html.erb +39 -0
  127. data/web/views/scheduled.html.erb +64 -0
  128. data/web/views/{scheduled_job_info.erb → scheduled_job_info.html.erb} +3 -3
  129. metadata +65 -47
  130. data/lib/sidekiq/web/csrf_protection.rb +0 -183
  131. data/web/assets/stylesheets/application-dark.css +0 -147
  132. data/web/assets/stylesheets/application-rtl.css +0 -163
  133. data/web/assets/stylesheets/application.css +0 -759
  134. data/web/assets/stylesheets/bootstrap-rtl.min.css +0 -9
  135. data/web/assets/stylesheets/bootstrap.css +0 -5
  136. data/web/views/_footer.erb +0 -34
  137. data/web/views/_job_info.erb +0 -105
  138. data/web/views/_metrics_period_select.erb +0 -12
  139. data/web/views/_nav.erb +0 -52
  140. data/web/views/_paging.erb +0 -25
  141. data/web/views/_poll_link.erb +0 -4
  142. data/web/views/_status.erb +0 -4
  143. data/web/views/busy.erb +0 -148
  144. data/web/views/dashboard.erb +0 -108
  145. data/web/views/dead.erb +0 -34
  146. data/web/views/filtering.erb +0 -6
  147. data/web/views/layout.erb +0 -42
  148. data/web/views/metrics.erb +0 -90
  149. data/web/views/metrics_for_job.erb +0 -59
  150. data/web/views/morgue.erb +0 -74
  151. data/web/views/queue.erb +0 -55
  152. data/web/views/queues.erb +0 -44
  153. data/web/views/retries.erb +0 -79
  154. data/web/views/retry.erb +0 -34
  155. data/web/views/scheduled.erb +0 -56
data/lib/sidekiq/rails.rb CHANGED
@@ -1,84 +1,63 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "sidekiq/job"
4
- require "rails"
5
-
6
3
  module Sidekiq
7
- module ActiveJob
8
- # @api private
9
- class Wrapper
10
- include Sidekiq::Job
11
-
12
- def perform(job_data)
13
- ::ActiveJob::Base.execute(job_data.merge("provider_job_id" => jid))
14
- end
15
- end
16
- end
17
-
18
- class Rails < ::Rails::Engine
19
- class Reloader
20
- def initialize(app = ::Rails.application)
21
- @app = app
22
- end
23
-
24
- def call
25
- params = (::Rails::VERSION::STRING >= "7.1") ? {source: "job.sidekiq"} : {}
26
- @app.reloader.wrap(**params) do
27
- 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
28
14
  end
29
- end
30
15
 
31
- def inspect
32
- "#<Sidekiq::Rails::Reloader @app=#{@app.class.name}>"
33
- 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
34
22
 
35
- def to_hash
36
- {app: @app.class.name}
37
- end
38
- end
23
+ def inspect
24
+ "#<Sidekiq::Rails::Reloader @app=#{@app.class.name}>"
25
+ end
39
26
 
40
- # By including the Options module, we allow AJs to directly control sidekiq features
41
- # via the *sidekiq_options* class method and, for instance, not use AJ's retry system.
42
- # AJ retries don't show up in the Sidekiq UI Retries tab, don't save any error data, can't be
43
- # manually retried, don't automatically die, etc.
44
- #
45
- # class SomeJob < ActiveJob::Base
46
- # queue_as :default
47
- # sidekiq_options retry: 3, backtrace: 10
48
- # def perform
49
- # end
50
- # end
51
- initializer "sidekiq.active_job_integration" do
52
- ActiveSupport.on_load(:active_job) do
53
- require_relative "../active_job/queue_adapters/sidekiq_adapter"
54
- include ::Sidekiq::Job::Options unless respond_to?(:sidekiq_options)
27
+ def to_hash
28
+ {app: @app.class.name}
29
+ end
55
30
  end
56
- end
57
31
 
58
- initializer "sidekiq.backtrace_cleaner" do
59
- Sidekiq.configure_server do |config|
60
- 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
61
36
  end
62
- end
63
-
64
- # This hook happens after all initializers are run, just before returning
65
- # from config/environment.rb back to sidekiq/cli.rb.
66
- #
67
- # None of this matters on the client-side, only within the Sidekiq process itself.
68
- config.after_initialize do
69
- Sidekiq.configure_server do |config|
70
- config[:reloader] = Sidekiq::Rails::Reloader.new
71
37
 
72
- # This is the integration code necessary so that if a job uses `Rails.logger.info "Hello"`,
73
- # it will appear in the Sidekiq console with all of the job context.
74
- unless ::Rails.logger == config.logger || ::ActiveSupport::Logger.logger_outputs_to?(::Rails.logger, $stdout)
75
- if ::Rails.logger.respond_to?(:broadcast_to)
76
- ::Rails.logger.broadcast_to(config.logger)
77
- else
78
- ::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
+ elsif ::ActiveSupport::Logger.respond_to?(:broadcast)
52
+ ::Rails.logger.extend(::ActiveSupport::Logger.broadcast(config.logger))
53
+ else
54
+ ::Rails.logger = ::ActiveSupport::BroadcastLogger.new(::Rails.logger, config.logger)
55
+ end
79
56
  end
80
57
  end
81
58
  end
82
59
  end
60
+ rescue Gem::LoadError
61
+ # Rails not available or version requirement not met
83
62
  end
84
63
  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
 
@@ -42,8 +41,8 @@ module Sidekiq
42
41
  # this allows us to use methods like `conn.hmset(...)` instead of having to use
43
42
  # redis-client's native `conn.call("hmset", ...)`
44
43
  def method_missing(*args, &block)
45
- warn("[sidekiq#5788] Redis has deprecated the `#{args.first}`command, called at #{caller(1..1)}") if DEPRECATED_COMMANDS.include?(args.first)
46
- @client.call(*args, *block)
44
+ warn("[sidekiq#5788] Redis has deprecated the `#{args.first}` command, called at #{caller(1..1)}") if DEPRECATED_COMMANDS.include?(args.first)
45
+ @client.call(*args, &block)
47
46
  end
48
47
  ruby2_keywords :method_missing if respond_to?(:ruby2_keywords, true)
49
48
 
@@ -108,6 +107,10 @@ module Sidekiq
108
107
  # on by default.
109
108
  opts[:reconnect_attempts] ||= 1
110
109
 
110
+ # Identify ourselves to Redis via CLIENT SETINFO so connections
111
+ # are distinguishable in CLIENT LIST / CLIENT INFO output.
112
+ opts[:driver_info] ||= "sidekiq_v#{Sidekiq::VERSION}"
113
+
111
114
  opts
112
115
  end
113
116
  end
@@ -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)}" }
@@ -21,7 +23,7 @@ module Sidekiq
21
23
 
22
24
  size = symbolized_options.delete(:size) || 5
23
25
  pool_timeout = symbolized_options.delete(:pool_timeout) || 1
24
- pool_name = symbolized_options.delete(:pool_name)
26
+ symbolized_options.delete(:pool_name)
25
27
 
26
28
  # Default timeout in redis-client is 1 second, which can be too aggressive
27
29
  # if the Sidekiq process is CPU-bound. With 10-15 threads and a thread quantum of 100ms,
@@ -31,13 +33,22 @@ module Sidekiq
31
33
  symbolized_options[:timeout] ||= 3
32
34
 
33
35
  redis_config = Sidekiq::RedisClientAdapter.new(symbolized_options)
34
- ConnectionPool.new(timeout: pool_timeout, size: size, name: pool_name) do
36
+ ConnectionPool.new(timeout: pool_timeout, size: size) do
35
37
  redis_config.new_client
36
38
  end
37
39
  end
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,14 +68,14 @@ 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
80
  if sentinel.is_a?(String)
70
81
  if (uri = URI(sentinel)) && uri.password
@@ -6,6 +6,7 @@ module Sidekiq
6
6
  class RingBuffer
7
7
  include Enumerable
8
8
  extend Forwardable
9
+
9
10
  def_delegators :@buf, :[], :each, :size
10
11
 
11
12
  def initialize(size, default = 0)
@@ -72,6 +72,7 @@ module Sidekiq
72
72
  include Sidekiq::Component
73
73
 
74
74
  INITIAL_WAIT = 10
75
+ attr_accessor :rnd
75
76
 
76
77
  def initialize(config)
77
78
  @config = config
@@ -80,6 +81,7 @@ module Sidekiq
80
81
  @done = false
81
82
  @thread = nil
82
83
  @count_calls = 0
84
+ @rnd = Random.new
83
85
  end
84
86
 
85
87
  # Shut down this instance, will pause until the thread is dead.
@@ -115,9 +117,7 @@ module Sidekiq
115
117
  private
116
118
 
117
119
  def wait
118
- @sleeper.pop(random_poll_interval)
119
- rescue Timeout::Error
120
- # expected
120
+ @sleeper.pop(timeout: random_poll_interval, exception: false)
121
121
  rescue => ex
122
122
  # if poll_interval_average hasn't been calculated yet, we can
123
123
  # raise an error trying to reach Redis.
@@ -151,11 +151,11 @@ module Sidekiq
151
151
 
152
152
  if count < 10
153
153
  # For small clusters, calculate a random interval that is ±50% the desired average.
154
- interval * rand + interval.to_f / 2
154
+ interval * @rnd.rand + interval.to_f / 2
155
155
  else
156
156
  # With 10+ processes, we should have enough randomness to get decent polling
157
157
  # across the entire timespan
158
- interval * rand
158
+ interval * @rnd.rand * 2
159
159
  end
160
160
  end
161
161
 
@@ -223,8 +223,7 @@ module Sidekiq
223
223
  total += INITIAL_WAIT unless @config[:poll_interval_average]
224
224
  total += (5 * rand)
225
225
 
226
- @sleeper.pop(total)
227
- rescue Timeout::Error
226
+ @sleeper.pop(timeout: total, exception: false)
228
227
  ensure
229
228
  # periodically clean out the `processes` set in Redis which can collect
230
229
  # references to dead processes over time. The process count affects how
@@ -0,0 +1,331 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "securerandom"
4
+ require "sidekiq"
5
+
6
+ module Sidekiq
7
+ class Testing
8
+ class TestModeAlreadySetError < RuntimeError; end
9
+ class << self
10
+ attr_accessor :__global_test_mode
11
+
12
+ # Calling without a block sets the global test mode, affecting
13
+ # all threads. Calling with a block only affects the current Thread.
14
+ def __set_test_mode(mode)
15
+ if block_given?
16
+ # Reentrant testing modes will lead to a rat's nest of code which is
17
+ # hard to reason about. You can set the testing mode once globally and
18
+ # you can override that global setting once per-thread.
19
+ raise TestModeAlreadySetError, "Nesting test modes is not supported" if __local_test_mode
20
+
21
+ self.__local_test_mode = mode
22
+ begin
23
+ yield
24
+ ensure
25
+ self.__local_test_mode = nil
26
+ end
27
+ else
28
+ self.__global_test_mode = mode
29
+ end
30
+ end
31
+
32
+ def __test_mode
33
+ __local_test_mode || __global_test_mode
34
+ end
35
+
36
+ def __local_test_mode
37
+ Thread.current[:__sidekiq_test_mode]
38
+ end
39
+
40
+ def __local_test_mode=(value)
41
+ Thread.current[:__sidekiq_test_mode] = value
42
+ end
43
+
44
+ def disable!(&block)
45
+ __set_test_mode(:disable, &block)
46
+ end
47
+
48
+ def fake!(&block)
49
+ __set_test_mode(:fake, &block)
50
+ end
51
+
52
+ def inline!(&block)
53
+ __set_test_mode(:inline, &block)
54
+ end
55
+
56
+ def enabled?
57
+ __test_mode != :disable
58
+ end
59
+
60
+ def disabled?
61
+ __test_mode == :disable
62
+ end
63
+
64
+ def fake?
65
+ __test_mode == :fake
66
+ end
67
+
68
+ def inline?
69
+ __test_mode == :inline
70
+ end
71
+
72
+ def server_middleware
73
+ @server_chain ||= Middleware::Chain.new(Sidekiq.default_configuration)
74
+ yield @server_chain if block_given?
75
+ @server_chain
76
+ end
77
+ end
78
+ end
79
+
80
+ class EmptyQueueError < RuntimeError; end
81
+
82
+ module TestingClient
83
+ private def atomic_push(conn, payloads)
84
+ if Sidekiq::Testing.fake?
85
+ payloads.each do |job|
86
+ job = Sidekiq.load_json(Sidekiq.dump_json(job))
87
+ job["enqueued_at"] = ::Process.clock_gettime(::Process::CLOCK_REALTIME, :millisecond) unless job["at"]
88
+ Queues.push(job["queue"], job["class"], job)
89
+ end
90
+ true
91
+ elsif Sidekiq::Testing.inline?
92
+ payloads.each do |job|
93
+ klass = Object.const_get(job["class"])
94
+ job["id"] ||= SecureRandom.hex(12)
95
+ job_hash = Sidekiq.load_json(Sidekiq.dump_json(job))
96
+ klass.process_job(job_hash)
97
+ end
98
+ true
99
+ else
100
+ super
101
+ end
102
+ end
103
+ end
104
+
105
+ Sidekiq::Client.prepend TestingClient
106
+
107
+ module Queues
108
+ ##
109
+ # The Queues class is only for testing the fake queue implementation.
110
+ # There are 2 data structures involved in tandem. This is due to the
111
+ # Rspec syntax of change(HardJob.jobs, :size). It keeps a reference
112
+ # to the array. Because the array was derived from a filter of the total
113
+ # jobs enqueued, it appeared as though the array didn't change.
114
+ #
115
+ # To solve this, we'll keep 2 hashes containing the jobs. One with keys based
116
+ # on the queue, and another with keys of the job type, so the array for
117
+ # HardJob.jobs is a straight reference to a real array.
118
+ #
119
+ # Queue-based hash:
120
+ #
121
+ # {
122
+ # "default"=>[
123
+ # {
124
+ # "class"=>"TestTesting::HardJob",
125
+ # "args"=>[1, 2],
126
+ # "retry"=>true,
127
+ # "queue"=>"default",
128
+ # "jid"=>"abc5b065c5c4b27fc1102833",
129
+ # "created_at"=>1447445554.419934
130
+ # }
131
+ # ]
132
+ # }
133
+ #
134
+ # Job-based hash:
135
+ #
136
+ # {
137
+ # "TestTesting::HardJob"=>[
138
+ # {
139
+ # "class"=>"TestTesting::HardJob",
140
+ # "args"=>[1, 2],
141
+ # "retry"=>true,
142
+ # "queue"=>"default",
143
+ # "jid"=>"abc5b065c5c4b27fc1102833",
144
+ # "created_at"=>1447445554.419934
145
+ # }
146
+ # ]
147
+ # }
148
+ #
149
+ # Example:
150
+ #
151
+ # require 'sidekiq/testing'
152
+ #
153
+ # assert_equal 0, Sidekiq::Queues["default"].size
154
+ # HardJob.perform_async(:something)
155
+ # assert_equal 1, Sidekiq::Queues["default"].size
156
+ # assert_equal :something, Sidekiq::Queues["default"].first['args'][0]
157
+ #
158
+ # You can also clear all jobs:
159
+ #
160
+ # assert_equal 0, Sidekiq::Queues["default"].size
161
+ # HardJob.perform_async(:something)
162
+ # Sidekiq::Queues.clear_all
163
+ # assert_equal 0, Sidekiq::Queues["default"].size
164
+ #
165
+ # This can be useful to make sure jobs don't linger between tests:
166
+ #
167
+ # RSpec.configure do |config|
168
+ # config.before(:each) do
169
+ # Sidekiq::Queues.clear_all
170
+ # end
171
+ # end
172
+ #
173
+ class << self
174
+ def [](queue)
175
+ jobs_by_queue[queue]
176
+ end
177
+
178
+ def push(queue, klass, job)
179
+ jobs_by_queue[queue] << job
180
+ jobs_by_class[klass] << job
181
+ end
182
+
183
+ def jobs_by_queue
184
+ @jobs_by_queue ||= Hash.new { |hash, key| hash[key] = [] }
185
+ end
186
+
187
+ def jobs_by_class
188
+ @jobs_by_class ||= Hash.new { |hash, key| hash[key] = [] }
189
+ end
190
+ alias_method :jobs_by_worker, :jobs_by_class
191
+
192
+ def delete_for(jid, queue, klass)
193
+ jobs_by_queue[queue.to_s].delete_if { |job| job["jid"] == jid }
194
+ jobs_by_class[klass].delete_if { |job| job["jid"] == jid }
195
+ end
196
+
197
+ def clear_for(queue, klass)
198
+ jobs_by_queue[queue.to_s].clear
199
+ jobs_by_class[klass].clear
200
+ end
201
+
202
+ def clear_all
203
+ jobs_by_queue.clear
204
+ jobs_by_class.clear
205
+ end
206
+ end
207
+ end
208
+
209
+ module Job
210
+ ##
211
+ # The Sidekiq testing infrastructure overrides perform_async
212
+ # so that it does not actually touch the network. Instead it
213
+ # stores the asynchronous jobs in a per-class array so that
214
+ # their presence/absence can be asserted by your tests.
215
+ #
216
+ # This is similar to ActionMailer's :test delivery_method and its
217
+ # ActionMailer::Base.deliveries array.
218
+ #
219
+ # Example:
220
+ #
221
+ # require 'sidekiq/testing'
222
+ #
223
+ # assert_equal 0, HardJob.jobs.size
224
+ # HardJob.perform_async(:something)
225
+ # assert_equal 1, HardJob.jobs.size
226
+ # assert_equal :something, HardJob.jobs[0]['args'][0]
227
+ #
228
+ # You can also clear and drain all job types:
229
+ #
230
+ # Sidekiq::Job.clear_all # or .drain_all
231
+ #
232
+ # This can be useful to make sure jobs don't linger between tests:
233
+ #
234
+ # RSpec.configure do |config|
235
+ # config.before(:each) do
236
+ # Sidekiq::Job.clear_all
237
+ # end
238
+ # end
239
+ #
240
+ # or for acceptance testing, i.e. with cucumber:
241
+ #
242
+ # AfterStep do
243
+ # Sidekiq::Job.drain_all
244
+ # end
245
+ #
246
+ # When I sign up as "foo@example.com"
247
+ # Then I should receive a welcome email to "foo@example.com"
248
+ #
249
+ module ClassMethods
250
+ # Queue for this worker
251
+ def queue
252
+ get_sidekiq_options["queue"]
253
+ end
254
+
255
+ # Jobs queued for this worker
256
+ def jobs
257
+ Queues.jobs_by_class[to_s]
258
+ end
259
+
260
+ # Clear all jobs for this worker
261
+ def clear
262
+ Queues.clear_for(queue, to_s)
263
+ end
264
+
265
+ # Drain and run all jobs for this worker
266
+ def drain
267
+ while jobs.any?
268
+ next_job = jobs.first
269
+ Queues.delete_for(next_job["jid"], next_job["queue"], to_s)
270
+ process_job(next_job)
271
+ end
272
+ end
273
+
274
+ # Pop out a single job and perform it
275
+ def perform_one
276
+ raise(EmptyQueueError, "perform_one called with empty job queue") if jobs.empty?
277
+ next_job = jobs.first
278
+ Queues.delete_for(next_job["jid"], next_job["queue"], to_s)
279
+ process_job(next_job)
280
+ end
281
+
282
+ def process_job(job)
283
+ instance = new
284
+ instance.jid = job["jid"]
285
+ instance.bid = job["bid"] if instance.respond_to?(:bid=)
286
+ Sidekiq::Testing.server_middleware.invoke(instance, job, job["queue"]) do
287
+ execute_job(instance, job["args"])
288
+ end
289
+ end
290
+
291
+ def execute_job(worker, args)
292
+ worker.perform(*args)
293
+ end
294
+ end
295
+
296
+ class << self
297
+ def jobs # :nodoc:
298
+ Queues.jobs_by_queue.values.flatten
299
+ end
300
+
301
+ # Clear all queued jobs
302
+ def clear_all
303
+ Queues.clear_all
304
+ end
305
+
306
+ # Drain (execute) all queued jobs
307
+ def drain_all
308
+ while jobs.any?
309
+ job_classes = jobs.map { |job| job["class"] }.uniq
310
+
311
+ job_classes.each do |job_class|
312
+ Object.const_get(job_class).drain
313
+ end
314
+ end
315
+ end
316
+ end
317
+ end
318
+
319
+ module TestingExtensions
320
+ def jobs_for(klass)
321
+ jobs.select do |job|
322
+ marshalled = job["args"][0]
323
+ marshalled.index(klass.to_s) && YAML.safe_load(marshalled)[0] == klass
324
+ end
325
+ end
326
+ end
327
+ end
328
+
329
+ if defined?(::Rails) && Rails.respond_to?(:env) && !Rails.env.test? && !$TESTING # rubocop:disable Style/GlobalVars
330
+ warn("⛔️ WARNING: Sidekiq testing API enabled, but this is not the test environment. Your jobs will not go to Redis.", uplevel: 1)
331
+ end
@@ -1,30 +1,2 @@
1
- # frozen_string_literal: true
2
-
3
- require "sidekiq/testing"
4
-
5
- ##
6
- # The Sidekiq inline infrastructure overrides perform_async so that it
7
- # actually calls perform instead. This allows jobs to be run inline in a
8
- # testing environment.
9
- #
10
- # This is similar to `Resque.inline = true` functionality.
11
- #
12
- # Example:
13
- #
14
- # require 'sidekiq/testing/inline'
15
- #
16
- # $external_variable = 0
17
- #
18
- # class ExternalJob
19
- # include Sidekiq::Job
20
- #
21
- # def perform
22
- # $external_variable = 1
23
- # end
24
- # end
25
- #
26
- # assert_equal 0, $external_variable
27
- # ExternalJob.perform_async
28
- # assert_equal 1, $external_variable
29
- #
30
- Sidekiq::Testing.inline!
1
+ Sidekiq.testing!(:inline)
2
+ warn('⛔️ `require "sidekiq/testing/inline"` is deprecated and will be removed in Sidekiq 9.0. See https://sidekiq.org/wiki/Testing#new-api')