sidekiq 3.5.4 → 5.2.7

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 (175) hide show
  1. checksums.yaml +5 -5
  2. data/.circleci/config.yml +61 -0
  3. data/{Contributing.md → .github/contributing.md} +0 -0
  4. data/.github/issue_template.md +11 -0
  5. data/.gitignore +3 -0
  6. data/.travis.yml +5 -10
  7. data/4.0-Upgrade.md +53 -0
  8. data/5.0-Upgrade.md +56 -0
  9. data/COMM-LICENSE +13 -11
  10. data/Changes.md +376 -1
  11. data/Ent-Changes.md +201 -2
  12. data/Gemfile +14 -18
  13. data/LICENSE +1 -1
  14. data/Pro-3.0-Upgrade.md +44 -0
  15. data/Pro-4.0-Upgrade.md +35 -0
  16. data/Pro-Changes.md +307 -2
  17. data/README.md +34 -22
  18. data/Rakefile +3 -3
  19. data/bin/sidekiq +0 -1
  20. data/bin/sidekiqctl +13 -86
  21. data/bin/sidekiqload +23 -27
  22. data/code_of_conduct.md +50 -0
  23. data/lib/generators/sidekiq/templates/worker_spec.rb.erb +3 -3
  24. data/lib/generators/sidekiq/templates/worker_test.rb.erb +6 -6
  25. data/lib/sidekiq.rb +72 -25
  26. data/lib/sidekiq/api.rb +206 -73
  27. data/lib/sidekiq/cli.rb +145 -101
  28. data/lib/sidekiq/client.rb +42 -36
  29. data/lib/sidekiq/core_ext.rb +1 -105
  30. data/lib/sidekiq/ctl.rb +221 -0
  31. data/lib/sidekiq/delay.rb +42 -0
  32. data/lib/sidekiq/exception_handler.rb +4 -5
  33. data/lib/sidekiq/extensions/action_mailer.rb +1 -0
  34. data/lib/sidekiq/extensions/active_record.rb +1 -0
  35. data/lib/sidekiq/extensions/class_methods.rb +1 -0
  36. data/lib/sidekiq/extensions/generic_proxy.rb +8 -1
  37. data/lib/sidekiq/fetch.rb +36 -111
  38. data/lib/sidekiq/job_logger.rb +25 -0
  39. data/lib/sidekiq/job_retry.rb +262 -0
  40. data/lib/sidekiq/launcher.rb +129 -55
  41. data/lib/sidekiq/logging.rb +21 -3
  42. data/lib/sidekiq/manager.rb +83 -182
  43. data/lib/sidekiq/middleware/chain.rb +1 -0
  44. data/lib/sidekiq/middleware/i18n.rb +1 -0
  45. data/lib/sidekiq/middleware/server/active_record.rb +10 -0
  46. data/lib/sidekiq/paginator.rb +1 -0
  47. data/lib/sidekiq/processor.rb +221 -103
  48. data/lib/sidekiq/rails.rb +47 -27
  49. data/lib/sidekiq/redis_connection.rb +74 -7
  50. data/lib/sidekiq/scheduled.rb +87 -28
  51. data/lib/sidekiq/testing.rb +150 -19
  52. data/lib/sidekiq/testing/inline.rb +1 -0
  53. data/lib/sidekiq/util.rb +15 -17
  54. data/lib/sidekiq/version.rb +2 -1
  55. data/lib/sidekiq/web.rb +120 -184
  56. data/lib/sidekiq/web/action.rb +89 -0
  57. data/lib/sidekiq/web/application.rb +353 -0
  58. data/lib/sidekiq/{web_helpers.rb → web/helpers.rb} +123 -47
  59. data/lib/sidekiq/web/router.rb +100 -0
  60. data/lib/sidekiq/worker.rb +135 -18
  61. data/sidekiq.gemspec +8 -14
  62. data/web/assets/images/{status-sd8051fd480.png → status.png} +0 -0
  63. data/web/assets/javascripts/application.js +24 -20
  64. data/web/assets/javascripts/dashboard.js +33 -18
  65. data/web/assets/stylesheets/application-rtl.css +246 -0
  66. data/web/assets/stylesheets/application.css +401 -7
  67. data/web/assets/stylesheets/bootstrap-rtl.min.css +9 -0
  68. data/web/assets/stylesheets/bootstrap.css +4 -8
  69. data/web/locales/ar.yml +81 -0
  70. data/web/locales/cs.yml +11 -1
  71. data/web/locales/de.yml +1 -1
  72. data/web/locales/en.yml +4 -0
  73. data/web/locales/es.yml +4 -3
  74. data/web/locales/fa.yml +80 -0
  75. data/web/locales/fr.yml +21 -12
  76. data/web/locales/he.yml +79 -0
  77. data/web/locales/ja.yml +24 -13
  78. data/web/locales/ru.yml +3 -0
  79. data/web/locales/ur.yml +80 -0
  80. data/web/views/_footer.erb +7 -9
  81. data/web/views/_job_info.erb +5 -1
  82. data/web/views/_nav.erb +5 -19
  83. data/web/views/_paging.erb +1 -1
  84. data/web/views/busy.erb +18 -9
  85. data/web/views/dashboard.erb +5 -5
  86. data/web/views/dead.erb +1 -1
  87. data/web/views/layout.erb +13 -5
  88. data/web/views/morgue.erb +16 -12
  89. data/web/views/queue.erb +12 -11
  90. data/web/views/queues.erb +5 -3
  91. data/web/views/retries.erb +19 -13
  92. data/web/views/retry.erb +2 -2
  93. data/web/views/scheduled.erb +4 -4
  94. data/web/views/scheduled_job_info.erb +1 -1
  95. metadata +45 -227
  96. data/lib/sidekiq/actor.rb +0 -39
  97. data/lib/sidekiq/middleware/server/logging.rb +0 -40
  98. data/lib/sidekiq/middleware/server/retry_jobs.rb +0 -206
  99. data/test/config.yml +0 -9
  100. data/test/env_based_config.yml +0 -11
  101. data/test/fake_env.rb +0 -0
  102. data/test/fixtures/en.yml +0 -2
  103. data/test/helper.rb +0 -49
  104. data/test/test_api.rb +0 -493
  105. data/test/test_cli.rb +0 -335
  106. data/test/test_client.rb +0 -194
  107. data/test/test_exception_handler.rb +0 -55
  108. data/test/test_extensions.rb +0 -126
  109. data/test/test_fetch.rb +0 -104
  110. data/test/test_logging.rb +0 -34
  111. data/test/test_manager.rb +0 -168
  112. data/test/test_middleware.rb +0 -159
  113. data/test/test_processor.rb +0 -237
  114. data/test/test_rails.rb +0 -21
  115. data/test/test_redis_connection.rb +0 -126
  116. data/test/test_retry.rb +0 -325
  117. data/test/test_scheduled.rb +0 -114
  118. data/test/test_scheduling.rb +0 -49
  119. data/test/test_sidekiq.rb +0 -99
  120. data/test/test_testing.rb +0 -142
  121. data/test/test_testing_fake.rb +0 -268
  122. data/test/test_testing_inline.rb +0 -93
  123. data/test/test_util.rb +0 -16
  124. data/test/test_web.rb +0 -608
  125. data/test/test_web_helpers.rb +0 -53
  126. data/web/assets/images/bootstrap/glyphicons-halflings-white.png +0 -0
  127. data/web/assets/images/bootstrap/glyphicons-halflings.png +0 -0
  128. data/web/assets/images/status/active.png +0 -0
  129. data/web/assets/images/status/idle.png +0 -0
  130. data/web/assets/javascripts/locales/README.md +0 -27
  131. data/web/assets/javascripts/locales/jquery.timeago.ar.js +0 -96
  132. data/web/assets/javascripts/locales/jquery.timeago.bg.js +0 -18
  133. data/web/assets/javascripts/locales/jquery.timeago.bs.js +0 -49
  134. data/web/assets/javascripts/locales/jquery.timeago.ca.js +0 -18
  135. data/web/assets/javascripts/locales/jquery.timeago.cs.js +0 -18
  136. data/web/assets/javascripts/locales/jquery.timeago.cy.js +0 -20
  137. data/web/assets/javascripts/locales/jquery.timeago.da.js +0 -18
  138. data/web/assets/javascripts/locales/jquery.timeago.de.js +0 -18
  139. data/web/assets/javascripts/locales/jquery.timeago.el.js +0 -18
  140. data/web/assets/javascripts/locales/jquery.timeago.en-short.js +0 -20
  141. data/web/assets/javascripts/locales/jquery.timeago.en.js +0 -20
  142. data/web/assets/javascripts/locales/jquery.timeago.es.js +0 -18
  143. data/web/assets/javascripts/locales/jquery.timeago.et.js +0 -18
  144. data/web/assets/javascripts/locales/jquery.timeago.fa.js +0 -22
  145. data/web/assets/javascripts/locales/jquery.timeago.fi.js +0 -28
  146. data/web/assets/javascripts/locales/jquery.timeago.fr-short.js +0 -16
  147. data/web/assets/javascripts/locales/jquery.timeago.fr.js +0 -17
  148. data/web/assets/javascripts/locales/jquery.timeago.he.js +0 -18
  149. data/web/assets/javascripts/locales/jquery.timeago.hr.js +0 -49
  150. data/web/assets/javascripts/locales/jquery.timeago.hu.js +0 -18
  151. data/web/assets/javascripts/locales/jquery.timeago.hy.js +0 -18
  152. data/web/assets/javascripts/locales/jquery.timeago.id.js +0 -18
  153. data/web/assets/javascripts/locales/jquery.timeago.it.js +0 -16
  154. data/web/assets/javascripts/locales/jquery.timeago.ja.js +0 -19
  155. data/web/assets/javascripts/locales/jquery.timeago.ko.js +0 -17
  156. data/web/assets/javascripts/locales/jquery.timeago.lt.js +0 -20
  157. data/web/assets/javascripts/locales/jquery.timeago.mk.js +0 -20
  158. data/web/assets/javascripts/locales/jquery.timeago.nl.js +0 -20
  159. data/web/assets/javascripts/locales/jquery.timeago.no.js +0 -18
  160. data/web/assets/javascripts/locales/jquery.timeago.pl.js +0 -31
  161. data/web/assets/javascripts/locales/jquery.timeago.pt-br.js +0 -16
  162. data/web/assets/javascripts/locales/jquery.timeago.pt.js +0 -16
  163. data/web/assets/javascripts/locales/jquery.timeago.ro.js +0 -18
  164. data/web/assets/javascripts/locales/jquery.timeago.rs.js +0 -49
  165. data/web/assets/javascripts/locales/jquery.timeago.ru.js +0 -34
  166. data/web/assets/javascripts/locales/jquery.timeago.sk.js +0 -18
  167. data/web/assets/javascripts/locales/jquery.timeago.sl.js +0 -44
  168. data/web/assets/javascripts/locales/jquery.timeago.sv.js +0 -18
  169. data/web/assets/javascripts/locales/jquery.timeago.th.js +0 -20
  170. data/web/assets/javascripts/locales/jquery.timeago.tr.js +0 -16
  171. data/web/assets/javascripts/locales/jquery.timeago.uk.js +0 -34
  172. data/web/assets/javascripts/locales/jquery.timeago.uz.js +0 -19
  173. data/web/assets/javascripts/locales/jquery.timeago.zh-cn.js +0 -20
  174. data/web/assets/javascripts/locales/jquery.timeago.zh-tw.js +0 -20
  175. data/web/views/_poll_js.erb +0 -5
@@ -1,38 +1,58 @@
1
- module Sidekiq
2
- def self.hook_rails!
3
- return if defined?(@delay_removed)
1
+ # frozen_string_literal: true
4
2
 
5
- ActiveSupport.on_load(:active_record) do
6
- include Sidekiq::Extensions::ActiveRecord
3
+ module Sidekiq
4
+ class Rails < ::Rails::Engine
5
+ # We need to setup this up before any application configuration which might
6
+ # change Sidekiq middleware.
7
+ #
8
+ # This hook happens after `Rails::Application` is inherited within
9
+ # config/application.rb and before config is touched, usually within the
10
+ # class block. Definitely before config/environments/*.rb and
11
+ # config/initializers/*.rb.
12
+ config.before_configuration do
13
+ if ::Rails::VERSION::MAJOR < 5 && defined?(::ActiveRecord)
14
+ Sidekiq.server_middleware do |chain|
15
+ require 'sidekiq/middleware/server/active_record'
16
+ chain.add Sidekiq::Middleware::Server::ActiveRecord
17
+ end
18
+ end
7
19
  end
8
20
 
9
- ActiveSupport.on_load(:action_mailer) do
10
- extend Sidekiq::Extensions::ActionMailer
21
+ config.after_initialize do
22
+ # This hook happens after all initializers are run, just before returning
23
+ # from config/environment.rb back to sidekiq/cli.rb.
24
+ # We have to add the reloader after initialize to see if cache_classes has
25
+ # been turned on.
26
+ #
27
+ # None of this matters on the client-side, only within the Sidekiq process itself.
28
+ #
29
+ Sidekiq.configure_server do |_|
30
+ if ::Rails::VERSION::MAJOR >= 5
31
+ Sidekiq.options[:reloader] = Sidekiq::Rails::Reloader.new
32
+ end
33
+ end
11
34
  end
12
35
 
13
- Module.__send__(:include, Sidekiq::Extensions::Klass)
14
- end
15
-
16
- # Removes the generic aliases which MAY clash with names of already
17
- # created methods by other applications. The methods `sidekiq_delay`,
18
- # `sidekiq_delay_for` and `sidekiq_delay_until` can be used instead.
19
- def self.remove_delay!
20
- @delay_removed = true
36
+ class Reloader
37
+ def initialize(app = ::Rails.application)
38
+ @app = app
39
+ end
21
40
 
22
- [Extensions::ActiveRecord,
23
- Extensions::ActionMailer,
24
- Extensions::Klass].each do |mod|
25
- mod.module_eval do
26
- remove_method :delay if respond_to?(:delay)
27
- remove_method :delay_for if respond_to?(:delay_for)
28
- remove_method :delay_until if respond_to?(:delay_until)
41
+ def call
42
+ @app.reloader.wrap do
43
+ yield
44
+ end
29
45
  end
30
- end
31
- end
32
46
 
33
- class Rails < ::Rails::Engine
34
- initializer 'sidekiq' do
35
- Sidekiq.hook_rails!
47
+ def inspect
48
+ "#<Sidekiq::Rails::Reloader @app=#{@app.class.name}>"
49
+ end
36
50
  end
37
51
  end if defined?(::Rails)
38
52
  end
53
+
54
+ if defined?(::Rails) && ::Rails::VERSION::MAJOR < 4
55
+ $stderr.puts("**************************************************")
56
+ $stderr.puts("⛔️ WARNING: Sidekiq server is no longer supported by Rails 3.2 - please ensure your server/workers are updated")
57
+ $stderr.puts("**************************************************")
58
+ end
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require 'connection_pool'
2
3
  require 'redis'
3
4
  require 'uri'
@@ -7,12 +8,26 @@ module Sidekiq
7
8
  class << self
8
9
 
9
10
  def create(options={})
11
+ options.keys.each do |key|
12
+ options[key.to_sym] = options.delete(key)
13
+ end
14
+
15
+ options[:id] = "Sidekiq-#{Sidekiq.server? ? "server" : "client"}-PID-#{$$}" if !options.has_key?(:id)
10
16
  options[:url] ||= determine_redis_provider
11
17
 
12
- # need a connection for Fetcher and Retry
13
- size = options[:size] || (Sidekiq.server? ? (Sidekiq.options[:concurrency] + 2) : 5)
14
- pool_timeout = options[:pool_timeout] || 1
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
27
+
28
+ verify_sizing(size, Sidekiq.options[:concurrency]) if Sidekiq.server?
15
29
 
30
+ pool_timeout = options[:pool_timeout] || 1
16
31
  log_info(options)
17
32
 
18
33
  ConnectionPool.new(:timeout => pool_timeout, :size => size) do
@@ -22,13 +37,31 @@ module Sidekiq
22
37
 
23
38
  private
24
39
 
40
+ # Sidekiq needs a lot of concurrent Redis connections.
41
+ #
42
+ # We need a connection for each Processor.
43
+ # We need a connection for Pro's real-time change listener
44
+ # We need a connection to various features to call Redis every few seconds:
45
+ # - the process heartbeat.
46
+ # - enterprise's leader election
47
+ # - enterprise's cron support
48
+ def verify_sizing(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
50
+ end
51
+
25
52
  def build_client(options)
26
53
  namespace = options[:namespace]
27
54
 
28
55
  client = Redis.new client_opts(options)
29
56
  if namespace
30
- require 'redis/namespace'
31
- Redis::Namespace.new(namespace, :redis => client)
57
+ begin
58
+ require 'redis/namespace'
59
+ Redis::Namespace.new(namespace, :redis => client)
60
+ rescue LoadError
61
+ Sidekiq.logger.error("Your Redis configuration uses the namespace '#{namespace}' but the redis-namespace gem is not included in the Gemfile." \
62
+ "Add the gem to your Gemfile to continue using a namespace. Otherwise, remove the namespace parameter.")
63
+ exit(-127)
64
+ end
32
65
  else
33
66
  client
34
67
  end
@@ -45,7 +78,14 @@ module Sidekiq
45
78
  opts.delete(:network_timeout)
46
79
  end
47
80
 
48
- opts[:driver] = opts[:driver] || 'ruby'
81
+ opts[:driver] ||= Redis::Connection.drivers.last || '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
49
89
 
50
90
  opts
51
91
  end
@@ -69,7 +109,34 @@ module Sidekiq
69
109
  end
70
110
 
71
111
  def determine_redis_provider
72
- 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
+ p = ENV['REDIS_PROVIDER']
119
+ if p && p =~ /\:/
120
+ Sidekiq.logger.error <<-EOM
121
+
122
+ #################################################################################
123
+
124
+ REDIS_PROVIDER should be set to the **name** of the variable which contains the Redis URL, not a URL itself.
125
+ Platforms like Heroku sell addons that publish a *_URL variable. You tell Sidekiq with REDIS_PROVIDER, e.g.:
126
+
127
+ REDIS_PROVIDER=REDISTOGO_URL
128
+ REDISTOGO_URL=redis://somehost.example.com:6379/4
129
+
130
+ Use REDIS_URL if you wish to point Sidekiq to a URL directly.
131
+
132
+ This configuration error will crash starting in Sidekiq 5.3.
133
+
134
+ #################################################################################
135
+ EOM
136
+ end
137
+ ENV[
138
+ ENV['REDIS_PROVIDER'] || 'REDIS_URL'
139
+ ]
73
140
  end
74
141
 
75
142
  end
@@ -1,6 +1,6 @@
1
+ # frozen_string_literal: true
1
2
  require 'sidekiq'
2
3
  require 'sidekiq/util'
3
- require 'sidekiq/actor'
4
4
  require 'sidekiq/api'
5
5
 
6
6
  module Sidekiq
@@ -39,36 +39,92 @@ module Sidekiq
39
39
  # workers can pick it up like any other job.
40
40
  class Poller
41
41
  include Util
42
- include Actor
43
42
 
44
43
  INITIAL_WAIT = 10
45
44
 
46
45
  def initialize
47
46
  @enq = (Sidekiq.options[:scheduled_enq] || Sidekiq::Scheduled::Enq).new
47
+ @sleeper = ConnectionPool::TimedStack.new
48
+ @done = false
49
+ @thread = nil
48
50
  end
49
51
 
50
- def poll(first_time=false)
51
- watchdog('scheduling poller thread died!') do
52
- initial_wait if first_time
53
-
54
- begin
55
- @enq.enqueue_jobs
56
- rescue => ex
57
- # Most likely a problem with redis networking.
58
- # Punt and try again at the next interval
59
- logger.error ex.message
60
- logger.error ex.backtrace.first
52
+ # Shut down this instance, will pause until the thread is dead.
53
+ def terminate
54
+ @done = true
55
+ if @thread
56
+ t = @thread
57
+ @thread = nil
58
+ @sleeper << 0
59
+ t.value
60
+ end
61
+ end
62
+
63
+ def start
64
+ @thread ||= safe_thread("scheduler") do
65
+ initial_wait
66
+
67
+ while !@done
68
+ enqueue
69
+ wait
61
70
  end
71
+ Sidekiq.logger.info("Scheduler exiting...")
72
+ end
73
+ end
62
74
 
63
- after(random_poll_interval) { poll }
75
+ def enqueue
76
+ begin
77
+ @enq.enqueue_jobs
78
+ rescue => ex
79
+ # Most likely a problem with redis networking.
80
+ # Punt and try again at the next interval
81
+ logger.error ex.message
82
+ handle_exception(ex)
64
83
  end
65
84
  end
66
85
 
67
86
  private
68
87
 
69
- # Calculates a random interval that is ±50% the desired average.
88
+ def wait
89
+ @sleeper.pop(random_poll_interval)
90
+ rescue Timeout::Error
91
+ # expected
92
+ rescue => ex
93
+ # if poll_interval_average hasn't been calculated yet, we can
94
+ # raise an error trying to reach Redis.
95
+ logger.error ex.message
96
+ handle_exception(ex)
97
+ sleep 5
98
+ end
99
+
70
100
  def random_poll_interval
71
- 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
72
128
  end
73
129
 
74
130
  # We do our best to tune the poll interval to the size of the active Sidekiq
@@ -83,7 +139,7 @@ module Sidekiq
83
139
  # all your Sidekiq processes at the same time will lead to them all polling at
84
140
  # the same time: the thundering herd problem.
85
141
  #
86
- # We only do this if poll_interval is unset (the default).
142
+ # We only do this if poll_interval_average is unset (the default).
87
143
  def poll_interval_average
88
144
  Sidekiq.options[:poll_interval_average] ||= scaled_poll_interval
89
145
  end
@@ -92,22 +148,25 @@ module Sidekiq
92
148
  # This minimizes a single point of failure by dispersing check-ins but without taxing
93
149
  # Redis if you run many Sidekiq processes.
94
150
  def scaled_poll_interval
151
+ process_count * Sidekiq.options[:average_scheduled_poll_interval]
152
+ end
153
+
154
+ def process_count
95
155
  pcount = Sidekiq::ProcessSet.new.size
96
156
  pcount = 1 if pcount == 0
97
- pcount * Sidekiq.options[:average_scheduled_poll_interval]
157
+ pcount
98
158
  end
99
159
 
100
160
  def initial_wait
101
- begin
102
- # Have all processes sleep between 5-15 seconds. 10 seconds
103
- # to give time for the heartbeat to register (if the poll interval is going to be calculated by the number
104
- # of workers), and 5 random seconds to ensure they don't all hit Redis at the same time.
105
- sleep(INITIAL_WAIT) unless Sidekiq.options[:poll_interval_average]
106
- sleep(5 * rand)
107
- rescue Celluloid::TaskTerminated
108
- # Hit Ctrl-C when Sidekiq is finished booting and we have a chance
109
- # to get here.
110
- end
161
+ # Have all processes sleep between 5-15 seconds. 10 seconds
162
+ # to give time for the heartbeat to register (if the poll interval is going to be calculated by the number
163
+ # of workers), and 5 random seconds to ensure they don't all hit Redis at the same time.
164
+ total = 0
165
+ total += INITIAL_WAIT unless Sidekiq.options[:poll_interval_average]
166
+ total += (5 * rand)
167
+
168
+ @sleeper.pop(total)
169
+ rescue Timeout::Error
111
170
  end
112
171
 
113
172
  end
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require 'securerandom'
2
3
  require 'sidekiq'
3
4
 
@@ -54,6 +55,15 @@ module Sidekiq
54
55
  yield @server_chain if block_given?
55
56
  @server_chain
56
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
57
67
  end
58
68
  end
59
69
 
@@ -62,25 +72,128 @@ module Sidekiq
62
72
 
63
73
  class EmptyQueueError < RuntimeError; end
64
74
 
65
- class Client
66
- alias_method :raw_push_real, :raw_push
67
-
75
+ module TestingClient
68
76
  def raw_push(payloads)
69
77
  if Sidekiq::Testing.fake?
70
78
  payloads.each do |job|
71
- job['class'].constantize.jobs << 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)
72
82
  end
73
83
  true
74
84
  elsif Sidekiq::Testing.inline?
75
85
  payloads.each do |job|
76
- job['jid'] ||= SecureRandom.hex(12)
77
- klass = job['class'].constantize
78
- klass.jobs.unshift Sidekiq.load_json(Sidekiq.dump_json(job))
79
- klass.perform_one
86
+ klass = Sidekiq::Testing.constantize(job['class'])
87
+ job['id'] ||= SecureRandom.hex(12)
88
+ job_hash = Sidekiq.load_json(Sidekiq.dump_json(job))
89
+ klass.process_job(job_hash)
80
90
  end
81
91
  true
82
92
  else
83
- raw_push_real(payloads)
93
+ super
94
+ end
95
+ end
96
+ end
97
+
98
+ Sidekiq::Client.prepend TestingClient
99
+
100
+ module Queues
101
+ ##
102
+ # The Queues class is only for testing the fake queue implementation.
103
+ # There are 2 data structures involved in tandem. This is due to the
104
+ # Rspec syntax of change(QueueWorker.jobs, :size). It keeps a reference
105
+ # to the array. Because the array was dervied from a filter of the total
106
+ # jobs enqueued, it appeared as though the array didn't change.
107
+ #
108
+ # To solve this, we'll keep 2 hashes containing the jobs. One with keys based
109
+ # on the queue, and another with keys of the worker names, so the array for
110
+ # QueueWorker.jobs is a straight reference to a real array.
111
+ #
112
+ # Queue-based hash:
113
+ #
114
+ # {
115
+ # "default"=>[
116
+ # {
117
+ # "class"=>"TestTesting::QueueWorker",
118
+ # "args"=>[1, 2],
119
+ # "retry"=>true,
120
+ # "queue"=>"default",
121
+ # "jid"=>"abc5b065c5c4b27fc1102833",
122
+ # "created_at"=>1447445554.419934
123
+ # }
124
+ # ]
125
+ # }
126
+ #
127
+ # Worker-based hash:
128
+ #
129
+ # {
130
+ # "TestTesting::QueueWorker"=>[
131
+ # {
132
+ # "class"=>"TestTesting::QueueWorker",
133
+ # "args"=>[1, 2],
134
+ # "retry"=>true,
135
+ # "queue"=>"default",
136
+ # "jid"=>"abc5b065c5c4b27fc1102833",
137
+ # "created_at"=>1447445554.419934
138
+ # }
139
+ # ]
140
+ # }
141
+ #
142
+ # Example:
143
+ #
144
+ # require 'sidekiq/testing'
145
+ #
146
+ # assert_equal 0, Sidekiq::Queues["default"].size
147
+ # HardWorker.perform_async(:something)
148
+ # assert_equal 1, Sidekiq::Queues["default"].size
149
+ # assert_equal :something, Sidekiq::Queues["default"].first['args'][0]
150
+ #
151
+ # You can also clear all workers' jobs:
152
+ #
153
+ # assert_equal 0, Sidekiq::Queues["default"].size
154
+ # HardWorker.perform_async(:something)
155
+ # Sidekiq::Queues.clear_all
156
+ # assert_equal 0, Sidekiq::Queues["default"].size
157
+ #
158
+ # This can be useful to make sure jobs don't linger between tests:
159
+ #
160
+ # RSpec.configure do |config|
161
+ # config.before(:each) do
162
+ # Sidekiq::Queues.clear_all
163
+ # end
164
+ # end
165
+ #
166
+ class << self
167
+ def [](queue)
168
+ jobs_by_queue[queue]
169
+ end
170
+
171
+ def push(queue, klass, job)
172
+ jobs_by_queue[queue] << job
173
+ jobs_by_worker[klass] << job
174
+ end
175
+
176
+ def jobs_by_queue
177
+ @jobs_by_queue ||= Hash.new { |hash, key| hash[key] = [] }
178
+ end
179
+
180
+ def jobs_by_worker
181
+ @jobs_by_worker ||= Hash.new { |hash, key| hash[key] = [] }
182
+ end
183
+
184
+ def delete_for(jid, queue, klass)
185
+ jobs_by_queue[queue.to_s].delete_if { |job| job["jid"] == jid }
186
+ jobs_by_worker[klass].delete_if { |job| job["jid"] == jid }
187
+ end
188
+
189
+ def clear_for(queue, klass)
190
+ jobs_by_queue[queue].clear
191
+ jobs_by_worker[klass].clear
192
+ end
193
+
194
+ def clear_all
195
+ jobs_by_queue.clear
196
+ jobs_by_worker.clear
84
197
  end
85
198
  end
86
199
  end
@@ -143,28 +256,36 @@ module Sidekiq
143
256
  #
144
257
  module ClassMethods
145
258
 
259
+ # Queue for this worker
260
+ def queue
261
+ self.sidekiq_options["queue"]
262
+ end
263
+
146
264
  # Jobs queued for this worker
147
265
  def jobs
148
- Worker.jobs[self]
266
+ Queues.jobs_by_worker[self.to_s]
149
267
  end
150
268
 
151
269
  # Clear all jobs for this worker
152
270
  def clear
153
- jobs.clear
271
+ Queues.clear_for(queue, self.to_s)
154
272
  end
155
273
 
156
274
  # Drain and run all jobs for this worker
157
275
  def drain
158
- while job = jobs.shift do
159
- process_job(job)
276
+ while jobs.any?
277
+ next_job = jobs.first
278
+ Queues.delete_for(next_job["jid"], next_job["queue"], self.to_s)
279
+ process_job(next_job)
160
280
  end
161
281
  end
162
282
 
163
283
  # Pop out a single job and perform it
164
284
  def perform_one
165
285
  raise(EmptyQueueError, "perform_one called with empty job queue") if jobs.empty?
166
- job = jobs.shift
167
- process_job(job)
286
+ next_job = jobs.first
287
+ Queues.delete_for(next_job["jid"], queue, self.to_s)
288
+ process_job(next_job)
168
289
  end
169
290
 
170
291
  def process_job(job)
@@ -183,20 +304,30 @@ module Sidekiq
183
304
 
184
305
  class << self
185
306
  def jobs # :nodoc:
186
- @jobs ||= Hash.new { |hash, key| hash[key] = [] }
307
+ Queues.jobs_by_queue.values.flatten
187
308
  end
188
309
 
189
310
  # Clear all queued jobs across all workers
190
311
  def clear_all
191
- jobs.clear
312
+ Queues.clear_all
192
313
  end
193
314
 
194
315
  # Drain all queued jobs across all workers
195
316
  def drain_all
196
- until jobs.values.all?(&:empty?) do
197
- jobs.keys.each(&:drain)
317
+ while jobs.any?
318
+ worker_classes = jobs.map { |job| job["class"] }.uniq
319
+
320
+ worker_classes.each do |worker_class|
321
+ Sidekiq::Testing.constantize(worker_class).drain
322
+ end
198
323
  end
199
324
  end
200
325
  end
201
326
  end
202
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