sidekiq 2.15.1 → 4.2.10

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 (187) hide show
  1. checksums.yaml +7 -0
  2. data/.github/contributing.md +32 -0
  3. data/.github/issue_template.md +9 -0
  4. data/.gitignore +1 -0
  5. data/.travis.yml +16 -17
  6. data/3.0-Upgrade.md +70 -0
  7. data/4.0-Upgrade.md +53 -0
  8. data/COMM-LICENSE +56 -44
  9. data/Changes.md +644 -1
  10. data/Ent-Changes.md +173 -0
  11. data/Gemfile +27 -0
  12. data/LICENSE +1 -1
  13. data/Pro-2.0-Upgrade.md +138 -0
  14. data/Pro-3.0-Upgrade.md +44 -0
  15. data/Pro-Changes.md +457 -3
  16. data/README.md +46 -29
  17. data/Rakefile +6 -3
  18. data/bin/sidekiq +4 -0
  19. data/bin/sidekiqctl +41 -20
  20. data/bin/sidekiqload +154 -0
  21. data/code_of_conduct.md +50 -0
  22. data/lib/generators/sidekiq/templates/worker.rb.erb +9 -0
  23. data/lib/generators/sidekiq/templates/worker_spec.rb.erb +6 -0
  24. data/lib/generators/sidekiq/templates/worker_test.rb.erb +8 -0
  25. data/lib/generators/sidekiq/worker_generator.rb +49 -0
  26. data/lib/sidekiq.rb +141 -29
  27. data/lib/sidekiq/api.rb +540 -106
  28. data/lib/sidekiq/cli.rb +131 -71
  29. data/lib/sidekiq/client.rb +168 -96
  30. data/lib/sidekiq/core_ext.rb +36 -8
  31. data/lib/sidekiq/exception_handler.rb +20 -28
  32. data/lib/sidekiq/extensions/action_mailer.rb +25 -5
  33. data/lib/sidekiq/extensions/active_record.rb +8 -4
  34. data/lib/sidekiq/extensions/class_methods.rb +9 -5
  35. data/lib/sidekiq/extensions/generic_proxy.rb +1 -0
  36. data/lib/sidekiq/fetch.rb +45 -101
  37. data/lib/sidekiq/launcher.rb +144 -30
  38. data/lib/sidekiq/logging.rb +69 -12
  39. data/lib/sidekiq/manager.rb +90 -140
  40. data/lib/sidekiq/middleware/chain.rb +18 -5
  41. data/lib/sidekiq/middleware/i18n.rb +9 -2
  42. data/lib/sidekiq/middleware/server/active_record.rb +1 -1
  43. data/lib/sidekiq/middleware/server/logging.rb +11 -11
  44. data/lib/sidekiq/middleware/server/retry_jobs.rb +98 -44
  45. data/lib/sidekiq/paginator.rb +20 -8
  46. data/lib/sidekiq/processor.rb +157 -96
  47. data/lib/sidekiq/rails.rb +109 -5
  48. data/lib/sidekiq/redis_connection.rb +70 -24
  49. data/lib/sidekiq/scheduled.rb +122 -50
  50. data/lib/sidekiq/testing.rb +171 -31
  51. data/lib/sidekiq/testing/inline.rb +1 -0
  52. data/lib/sidekiq/util.rb +31 -5
  53. data/lib/sidekiq/version.rb +2 -1
  54. data/lib/sidekiq/web.rb +136 -263
  55. data/lib/sidekiq/web/action.rb +93 -0
  56. data/lib/sidekiq/web/application.rb +336 -0
  57. data/lib/sidekiq/web/helpers.rb +278 -0
  58. data/lib/sidekiq/web/router.rb +100 -0
  59. data/lib/sidekiq/worker.rb +40 -7
  60. data/sidekiq.gemspec +18 -14
  61. data/web/assets/images/favicon.ico +0 -0
  62. data/web/assets/images/{status-sd8051fd480.png → status.png} +0 -0
  63. data/web/assets/javascripts/application.js +67 -19
  64. data/web/assets/javascripts/dashboard.js +138 -29
  65. data/web/assets/stylesheets/application.css +267 -406
  66. data/web/assets/stylesheets/bootstrap.css +4 -8
  67. data/web/locales/cs.yml +78 -0
  68. data/web/locales/da.yml +9 -1
  69. data/web/locales/de.yml +18 -9
  70. data/web/locales/el.yml +68 -0
  71. data/web/locales/en.yml +19 -4
  72. data/web/locales/es.yml +10 -1
  73. data/web/locales/fa.yml +79 -0
  74. data/web/locales/fr.yml +50 -32
  75. data/web/locales/hi.yml +75 -0
  76. data/web/locales/it.yml +27 -18
  77. data/web/locales/ja.yml +27 -12
  78. data/web/locales/ko.yml +8 -3
  79. data/web/locales/{no.yml → nb.yml} +19 -5
  80. data/web/locales/nl.yml +8 -3
  81. data/web/locales/pl.yml +0 -1
  82. data/web/locales/pt-br.yml +11 -4
  83. data/web/locales/pt.yml +8 -1
  84. data/web/locales/ru.yml +39 -21
  85. data/web/locales/sv.yml +68 -0
  86. data/web/locales/ta.yml +75 -0
  87. data/web/locales/uk.yml +76 -0
  88. data/web/locales/zh-cn.yml +68 -0
  89. data/web/locales/zh-tw.yml +68 -0
  90. data/web/views/_footer.erb +17 -0
  91. data/web/views/_job_info.erb +72 -60
  92. data/web/views/_nav.erb +58 -25
  93. data/web/views/_paging.erb +5 -5
  94. data/web/views/_poll_link.erb +7 -0
  95. data/web/views/_summary.erb +20 -14
  96. data/web/views/busy.erb +94 -0
  97. data/web/views/dashboard.erb +34 -21
  98. data/web/views/dead.erb +34 -0
  99. data/web/views/layout.erb +8 -30
  100. data/web/views/morgue.erb +75 -0
  101. data/web/views/queue.erb +37 -30
  102. data/web/views/queues.erb +26 -20
  103. data/web/views/retries.erb +60 -47
  104. data/web/views/retry.erb +23 -19
  105. data/web/views/scheduled.erb +39 -35
  106. data/web/views/scheduled_job_info.erb +2 -1
  107. metadata +152 -195
  108. data/Contributing.md +0 -29
  109. data/config.ru +0 -18
  110. data/lib/sidekiq/actor.rb +0 -7
  111. data/lib/sidekiq/capistrano.rb +0 -54
  112. data/lib/sidekiq/yaml_patch.rb +0 -21
  113. data/test/config.yml +0 -11
  114. data/test/env_based_config.yml +0 -11
  115. data/test/fake_env.rb +0 -0
  116. data/test/helper.rb +0 -42
  117. data/test/test_api.rb +0 -341
  118. data/test/test_cli.rb +0 -326
  119. data/test/test_client.rb +0 -211
  120. data/test/test_exception_handler.rb +0 -124
  121. data/test/test_extensions.rb +0 -105
  122. data/test/test_fetch.rb +0 -44
  123. data/test/test_manager.rb +0 -83
  124. data/test/test_middleware.rb +0 -135
  125. data/test/test_processor.rb +0 -160
  126. data/test/test_redis_connection.rb +0 -97
  127. data/test/test_retry.rb +0 -306
  128. data/test/test_scheduled.rb +0 -86
  129. data/test/test_scheduling.rb +0 -47
  130. data/test/test_sidekiq.rb +0 -37
  131. data/test/test_testing.rb +0 -82
  132. data/test/test_testing_fake.rb +0 -265
  133. data/test/test_testing_inline.rb +0 -92
  134. data/test/test_util.rb +0 -18
  135. data/test/test_web.rb +0 -372
  136. data/web/assets/images/bootstrap/glyphicons-halflings-white.png +0 -0
  137. data/web/assets/images/bootstrap/glyphicons-halflings.png +0 -0
  138. data/web/assets/images/status/active.png +0 -0
  139. data/web/assets/images/status/idle.png +0 -0
  140. data/web/assets/javascripts/locales/README.md +0 -27
  141. data/web/assets/javascripts/locales/jquery.timeago.ar.js +0 -96
  142. data/web/assets/javascripts/locales/jquery.timeago.bg.js +0 -18
  143. data/web/assets/javascripts/locales/jquery.timeago.bs.js +0 -49
  144. data/web/assets/javascripts/locales/jquery.timeago.ca.js +0 -18
  145. data/web/assets/javascripts/locales/jquery.timeago.cy.js +0 -20
  146. data/web/assets/javascripts/locales/jquery.timeago.cz.js +0 -18
  147. data/web/assets/javascripts/locales/jquery.timeago.da.js +0 -18
  148. data/web/assets/javascripts/locales/jquery.timeago.de.js +0 -18
  149. data/web/assets/javascripts/locales/jquery.timeago.el.js +0 -18
  150. data/web/assets/javascripts/locales/jquery.timeago.en-short.js +0 -20
  151. data/web/assets/javascripts/locales/jquery.timeago.en.js +0 -20
  152. data/web/assets/javascripts/locales/jquery.timeago.es.js +0 -18
  153. data/web/assets/javascripts/locales/jquery.timeago.et.js +0 -18
  154. data/web/assets/javascripts/locales/jquery.timeago.fa.js +0 -22
  155. data/web/assets/javascripts/locales/jquery.timeago.fi.js +0 -28
  156. data/web/assets/javascripts/locales/jquery.timeago.fr-short.js +0 -16
  157. data/web/assets/javascripts/locales/jquery.timeago.fr.js +0 -17
  158. data/web/assets/javascripts/locales/jquery.timeago.he.js +0 -18
  159. data/web/assets/javascripts/locales/jquery.timeago.hr.js +0 -49
  160. data/web/assets/javascripts/locales/jquery.timeago.hu.js +0 -18
  161. data/web/assets/javascripts/locales/jquery.timeago.hy.js +0 -18
  162. data/web/assets/javascripts/locales/jquery.timeago.id.js +0 -18
  163. data/web/assets/javascripts/locales/jquery.timeago.it.js +0 -16
  164. data/web/assets/javascripts/locales/jquery.timeago.ja.js +0 -19
  165. data/web/assets/javascripts/locales/jquery.timeago.ko.js +0 -17
  166. data/web/assets/javascripts/locales/jquery.timeago.lt.js +0 -20
  167. data/web/assets/javascripts/locales/jquery.timeago.mk.js +0 -20
  168. data/web/assets/javascripts/locales/jquery.timeago.nl.js +0 -20
  169. data/web/assets/javascripts/locales/jquery.timeago.no.js +0 -18
  170. data/web/assets/javascripts/locales/jquery.timeago.pl.js +0 -31
  171. data/web/assets/javascripts/locales/jquery.timeago.pt-br.js +0 -16
  172. data/web/assets/javascripts/locales/jquery.timeago.pt.js +0 -16
  173. data/web/assets/javascripts/locales/jquery.timeago.ro.js +0 -18
  174. data/web/assets/javascripts/locales/jquery.timeago.rs.js +0 -49
  175. data/web/assets/javascripts/locales/jquery.timeago.ru.js +0 -34
  176. data/web/assets/javascripts/locales/jquery.timeago.sk.js +0 -18
  177. data/web/assets/javascripts/locales/jquery.timeago.sl.js +0 -44
  178. data/web/assets/javascripts/locales/jquery.timeago.sv.js +0 -18
  179. data/web/assets/javascripts/locales/jquery.timeago.th.js +0 -20
  180. data/web/assets/javascripts/locales/jquery.timeago.tr.js +0 -16
  181. data/web/assets/javascripts/locales/jquery.timeago.uk.js +0 -34
  182. data/web/assets/javascripts/locales/jquery.timeago.uz.js +0 -19
  183. data/web/assets/javascripts/locales/jquery.timeago.zh-CN.js +0 -20
  184. data/web/assets/javascripts/locales/jquery.timeago.zh-TW.js +0 -20
  185. data/web/views/_poll.erb +0 -14
  186. data/web/views/_workers.erb +0 -29
  187. data/web/views/index.erb +0 -16
@@ -1,19 +1,123 @@
1
+ # frozen_string_literal: true
1
2
  module Sidekiq
2
3
  def self.hook_rails!
3
- if defined?(::ActiveRecord)
4
- ::ActiveRecord::Base.send(:include, Sidekiq::Extensions::ActiveRecord)
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
5
12
  end
6
13
 
7
- if defined?(::ActionMailer)
8
- ::ActionMailer::Base.extend(Sidekiq::Extensions::ActionMailer)
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
9
31
  end
10
32
  end
11
33
 
12
34
  class Rails < ::Rails::Engine
13
- config.autoload_paths << File.expand_path("#{config.root}/app/workers") if File.exist?("#{config.root}/app/workers")
35
+ # We need to setup this up before any application configuration which might
36
+ # change Sidekiq middleware.
37
+ #
38
+ # This hook happens after `Rails::Application` is inherited within
39
+ # config/application.rb and before config is touched, usually within the
40
+ # class block. Definitely before config/environments/*.rb and
41
+ # config/initializers/*.rb.
42
+ config.before_configuration do
43
+ if ::Rails::VERSION::MAJOR < 5 && defined?(::ActiveRecord)
44
+ Sidekiq.server_middleware do |chain|
45
+ require 'sidekiq/middleware/server/active_record'
46
+ chain.add Sidekiq::Middleware::Server::ActiveRecord
47
+ end
48
+ end
49
+ end
14
50
 
15
51
  initializer 'sidekiq' do
16
52
  Sidekiq.hook_rails!
17
53
  end
54
+
55
+ config.after_initialize do
56
+ # This hook happens after all initializers are run, just before returning
57
+ # from config/environment.rb back to sidekiq/cli.rb.
58
+ # We have to add the reloader after initialize to see if cache_classes has
59
+ # been turned on.
60
+ #
61
+ # None of this matters on the client-side, only within the Sidekiq process itself.
62
+ #
63
+ Sidekiq.configure_server do |_|
64
+ if ::Rails::VERSION::MAJOR >= 5
65
+ # The reloader also takes care of ActiveRecord but is incompatible with
66
+ # the ActiveRecord middleware so make sure it's not in the chain already.
67
+ if defined?(Sidekiq::Middleware::Server::ActiveRecord) && Sidekiq.server_middleware.exists?(Sidekiq::Middleware::Server::ActiveRecord)
68
+ 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."
69
+ elsif ::Rails.application.config.cache_classes
70
+ # The reloader API has proven to be troublesome under load in production.
71
+ # We won't use it at all when classes are cached, see #3154
72
+ Sidekiq.logger.debug { "Autoload disabled in #{::Rails.env}, Sidekiq will not reload changed classes" }
73
+ Sidekiq.options[:executor] = Sidekiq::Rails::Executor.new
74
+ else
75
+ Sidekiq.logger.debug { "Enabling Rails 5+ live code reloading, so hot!" }
76
+ Sidekiq.options[:reloader] = Sidekiq::Rails::Reloader.new
77
+ Psych::Visitors::ToRuby.prepend(Sidekiq::Rails::PsychAutoload)
78
+ end
79
+ end
80
+ end
81
+ end
82
+
83
+ class Executor
84
+ def initialize(app = ::Rails.application)
85
+ @app = app
86
+ end
87
+
88
+ def call
89
+ @app.executor.wrap do
90
+ yield
91
+ end
92
+ end
93
+
94
+ def inspect
95
+ "#<Sidekiq::Rails::Executor @app=#{@app.class.name}>"
96
+ end
97
+ end
98
+
99
+ class Reloader
100
+ def initialize(app = ::Rails.application)
101
+ @app = app
102
+ end
103
+
104
+ def call
105
+ @app.reloader.wrap do
106
+ yield
107
+ end
108
+ end
109
+
110
+ def inspect
111
+ "#<Sidekiq::Rails::Reloader @app=#{@app.class.name}>"
112
+ end
113
+ end
114
+
115
+ module PsychAutoload
116
+ def resolve_class(klass_name)
117
+ klass_name && klass_name.constantize
118
+ rescue NameError
119
+ super
120
+ end
121
+ end
18
122
  end if defined?(::Rails)
19
123
  end
@@ -1,58 +1,104 @@
1
+ # frozen_string_literal: true
1
2
  require 'connection_pool'
2
3
  require 'redis'
4
+ require 'uri'
3
5
 
4
6
  module Sidekiq
5
7
  class RedisConnection
6
8
  class << self
7
9
 
8
10
  def create(options={})
9
- url = options[:url] || determine_redis_provider || 'redis://localhost:6379/0'
10
- # need a connection for Fetcher and Retry
11
- size = options[:size] || (Sidekiq.server? ? (Sidekiq.options[:concurrency] + 2) : 5)
12
- pool_timeout = options[:pool_timeout] || 1
11
+ options = options.symbolize_keys
12
+
13
+ options[:url] ||= determine_redis_provider
14
+
15
+ size = options[:size] || (Sidekiq.server? ? (Sidekiq.options[:concurrency] + 5) : 5)
13
16
 
14
- log_info(url, options)
17
+ verify_sizing(size, Sidekiq.options[:concurrency]) if Sidekiq.server?
18
+
19
+ pool_timeout = options[:pool_timeout] || 1
20
+ log_info(options)
15
21
 
16
22
  ConnectionPool.new(:timeout => pool_timeout, :size => size) do
17
- build_client(url, options[:namespace], options[:driver] || 'ruby', options[:network_timeout])
23
+ build_client(options)
18
24
  end
19
25
  end
20
26
 
21
27
  private
22
28
 
23
- def build_client(url, namespace, driver, network_timeout)
24
- client = Redis.new client_opts(url, driver, network_timeout)
29
+ # Sidekiq needs a lot of concurrent Redis connections.
30
+ #
31
+ # We need a connection for each Processor.
32
+ # We need a connection for Pro's real-time change listener
33
+ # We need a connection to various features to call Redis every few seconds:
34
+ # - the process heartbeat.
35
+ # - enterprise's leader election
36
+ # - enterprise's cron support
37
+ 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
39
+ end
40
+
41
+ def build_client(options)
42
+ namespace = options[:namespace]
43
+
44
+ client = Redis.new client_opts(options)
25
45
  if namespace
26
- require 'redis/namespace'
27
- Redis::Namespace.new(namespace, :redis => client)
46
+ begin
47
+ require 'redis/namespace'
48
+ Redis::Namespace.new(namespace, :redis => client)
49
+ rescue LoadError
50
+ Sidekiq.logger.error("Your Redis configuration uses the namespace '#{namespace}' but the redis-namespace gem is not included in the Gemfile." \
51
+ "Add the gem to your Gemfile to continue using a namespace. Otherwise, remove the namespace parameter.")
52
+ exit(-127)
53
+ end
28
54
  else
29
55
  client
30
56
  end
31
57
  end
32
58
 
33
- def client_opts(url, driver, timeout)
34
- if timeout
35
- { :url => url, :driver => driver, :timeout => timeout }
36
- else
37
- { :url => url, :driver => driver }
59
+ def client_opts(options)
60
+ opts = options.dup
61
+ if opts[:namespace]
62
+ opts.delete(:namespace)
63
+ end
64
+
65
+ if opts[:network_timeout]
66
+ opts[:timeout] = opts[:network_timeout]
67
+ opts.delete(:network_timeout)
38
68
  end
69
+
70
+ opts[:driver] ||= 'ruby'
71
+
72
+ # Issue #3303, redis-rb will silently retry an operation.
73
+ # This can lead to duplicate jobs if Sidekiq::Client's LPUSH
74
+ # is performed twice but I believe this is much, much rarer
75
+ # than the reconnect silently fixing a problem; we keep it
76
+ # on by default.
77
+ opts[:reconnect_attempts] ||= 1
78
+
79
+ opts
39
80
  end
40
81
 
41
- def log_info(url, options)
42
- opts = options.dup
43
- opts.delete(:url)
82
+ def log_info(options)
83
+ # Don't log Redis AUTH password
84
+ redacted = "REDACTED"
85
+ scrubbed_options = options.dup
86
+ if scrubbed_options[:url] && (uri = URI.parse(scrubbed_options[:url])) && uri.password
87
+ uri.password = redacted
88
+ scrubbed_options[:url] = uri.to_s
89
+ end
90
+ if scrubbed_options[:password]
91
+ scrubbed_options[:password] = redacted
92
+ end
44
93
  if Sidekiq.server?
45
- Sidekiq.logger.info("Booting Sidekiq #{Sidekiq::VERSION} using #{url} with options #{opts}")
94
+ Sidekiq.logger.info("Booting Sidekiq #{Sidekiq::VERSION} with redis options #{scrubbed_options}")
46
95
  else
47
- Sidekiq.logger.info("#{Sidekiq::NAME} client using #{url} with options #{opts}")
96
+ Sidekiq.logger.debug("#{Sidekiq::NAME} client with redis options #{scrubbed_options}")
48
97
  end
49
98
  end
50
99
 
51
100
  def determine_redis_provider
52
- # REDISTOGO_URL is only support for legacy reasons
53
- return ENV['REDISTOGO_URL'] if ENV['REDISTOGO_URL']
54
- provider = ENV['REDIS_PROVIDER'] || 'REDIS_URL'
55
- ENV[provider]
101
+ ENV[ENV['REDIS_PROVIDER'] || 'REDIS_URL']
56
102
  end
57
103
 
58
104
  end
@@ -1,75 +1,147 @@
1
+ # frozen_string_literal: true
1
2
  require 'sidekiq'
2
3
  require 'sidekiq/util'
3
- require 'sidekiq/actor'
4
+ require 'sidekiq/api'
4
5
 
5
6
  module Sidekiq
6
7
  module Scheduled
8
+ SETS = %w(retry schedule)
7
9
 
8
- POLL_INTERVAL = 15
10
+ class Enq
11
+ def enqueue_jobs(now=Time.now.to_f.to_s, sorted_sets=SETS)
12
+ # A job's "score" in Redis is the time at which it should be processed.
13
+ # Just check Redis for the set of jobs with a timestamp before now.
14
+ Sidekiq.redis do |conn|
15
+ sorted_sets.each do |sorted_set|
16
+ # Get the next item in the queue if it's score (time to execute) is <= now.
17
+ # We need to go through the list one at a time to reduce the risk of something
18
+ # going wrong between the time jobs are popped from the scheduled queue and when
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
21
+
22
+ # Pop item off the queue and add it to the work queue. If the job can't be popped from
23
+ # the queue, it's because another process already popped it so we can move on to the
24
+ # next one.
25
+ if conn.zrem(sorted_set, job)
26
+ Sidekiq::Client.push(Sidekiq.load_json(job))
27
+ Sidekiq::Logging.logger.debug { "enqueued #{sorted_set}: #{job}" }
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
9
34
 
10
35
  ##
11
- # The Poller checks Redis every N seconds for messages in the retry or scheduled
36
+ # The Poller checks Redis every N seconds for jobs in the retry or scheduled
12
37
  # set have passed their timestamp and should be enqueued. If so, it
13
- # just pops the message back onto its original queue so the
14
- # workers can pick it up like any other message.
38
+ # just pops the job back onto its original queue so the
39
+ # workers can pick it up like any other job.
15
40
  class Poller
16
41
  include Util
17
- include Actor
18
-
19
- SETS = %w(retry schedule)
20
-
21
- def poll(first_time=false)
22
- watchdog('scheduling poller thread died!') do
23
- add_jitter if first_time
24
-
25
- begin
26
- # A message's "score" in Redis is the time at which it should be processed.
27
- # Just check Redis for the set of messages with a timestamp before now.
28
- now = Time.now.to_f.to_s
29
- Sidekiq.redis do |conn|
30
- SETS.each do |sorted_set|
31
- # Get the next item in the queue if it's score (time to execute) is <= now.
32
- # We need to go through the list one at a time to reduce the risk of something
33
- # going wrong between the time jobs are popped from the scheduled queue and when
34
- # they are pushed onto a work queue and losing the jobs.
35
- while message = conn.zrangebyscore(sorted_set, '-inf', now, :limit => [0, 1]).first do
36
-
37
- # Pop item off the queue and add it to the work queue. If the job can't be popped from
38
- # the queue, it's because another process already popped it so we can move on to the
39
- # next one.
40
- if conn.zrem(sorted_set, message)
41
- Sidekiq::Client.push(Sidekiq.load_json(message))
42
- logger.debug { "enqueued #{sorted_set}: #{message}" }
43
- end
44
- end
45
- end
46
- end
47
- rescue => ex
48
- # Most likely a problem with redis networking.
49
- # Punt and try again at the next interval
50
- logger.error ex.message
51
- logger.error ex.backtrace.first
52
- end
53
42
 
54
- after(poll_interval) { poll }
43
+ INITIAL_WAIT = 10
44
+
45
+ def initialize
46
+ @enq = (Sidekiq.options[:scheduled_enq] || Sidekiq::Scheduled::Enq).new
47
+ @sleeper = ConnectionPool::TimedStack.new
48
+ @done = false
49
+ @thread = nil
50
+ end
51
+
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
55
60
  end
56
61
  end
57
62
 
58
- private
63
+ def start
64
+ @thread ||= safe_thread("scheduler") do
65
+ initial_wait
59
66
 
60
- def poll_interval
61
- Sidekiq.options[:poll_interval] || POLL_INTERVAL
67
+ while !@done
68
+ enqueue
69
+ wait
70
+ end
71
+ Sidekiq.logger.info("Scheduler exiting...")
72
+ end
62
73
  end
63
74
 
64
- def add_jitter
75
+ def enqueue
65
76
  begin
66
- sleep(poll_interval * rand)
67
- rescue Celluloid::Task::TerminatedError
68
- # Hit Ctrl-C when Sidekiq is finished booting and we have a chance
69
- # to get here.
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
+ ex.backtrace.each do |bt|
83
+ logger.error(bt)
84
+ end
70
85
  end
71
86
  end
72
87
 
88
+ private
89
+
90
+ def wait
91
+ @sleeper.pop(random_poll_interval)
92
+ rescue Timeout::Error
93
+ # expected
94
+ rescue => ex
95
+ # if poll_interval_average hasn't been calculated yet, we can
96
+ # raise an error trying to reach Redis.
97
+ logger.error ex.message
98
+ logger.error ex.backtrace.first
99
+ sleep 5
100
+ end
101
+
102
+ # Calculates a random interval that is ±50% the desired average.
103
+ def random_poll_interval
104
+ poll_interval_average * rand + poll_interval_average.to_f / 2
105
+ end
106
+
107
+ # We do our best to tune the poll interval to the size of the active Sidekiq
108
+ # cluster. If you have 30 processes and poll every 15 seconds, that means one
109
+ # Sidekiq is checking Redis every 0.5 seconds - way too often for most people
110
+ # and really bad if the retry or scheduled sets are large.
111
+ #
112
+ # Instead try to avoid polling more than once every 15 seconds. If you have
113
+ # 30 Sidekiq processes, we'll poll every 30 * 15 or 450 seconds.
114
+ # To keep things statistically random, we'll sleep a random amount between
115
+ # 225 and 675 seconds for each poll or 450 seconds on average. Otherwise restarting
116
+ # all your Sidekiq processes at the same time will lead to them all polling at
117
+ # the same time: the thundering herd problem.
118
+ #
119
+ # We only do this if poll_interval_average is unset (the default).
120
+ def poll_interval_average
121
+ Sidekiq.options[:poll_interval_average] ||= scaled_poll_interval
122
+ end
123
+
124
+ # Calculates an average poll interval based on the number of known Sidekiq processes.
125
+ # This minimizes a single point of failure by dispersing check-ins but without taxing
126
+ # Redis if you run many Sidekiq processes.
127
+ def scaled_poll_interval
128
+ pcount = Sidekiq::ProcessSet.new.size
129
+ pcount = 1 if pcount == 0
130
+ pcount * Sidekiq.options[:average_scheduled_poll_interval]
131
+ end
132
+
133
+ def initial_wait
134
+ # Have all processes sleep between 5-15 seconds. 10 seconds
135
+ # to give time for the heartbeat to register (if the poll interval is going to be calculated by the number
136
+ # of workers), and 5 random seconds to ensure they don't all hit Redis at the same time.
137
+ total = 0
138
+ total += INITIAL_WAIT unless Sidekiq.options[:poll_interval_average]
139
+ total += (5 * rand)
140
+
141
+ @sleeper.pop(total)
142
+ rescue Timeout::Error
143
+ end
144
+
73
145
  end
74
146
  end
75
147
  end