sidekiq 3.4.1 → 4.0.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of sidekiq might be problematic. Click here for more details.

Files changed (83) hide show
  1. checksums.yaml +4 -4
  2. data/4.0-Upgrade.md +50 -0
  3. data/COMM-LICENSE +55 -45
  4. data/Changes.md +73 -1
  5. data/Ent-Changes.md +66 -0
  6. data/Gemfile +7 -1
  7. data/Pro-2.0-Upgrade.md +2 -2
  8. data/Pro-3.0-Upgrade.md +46 -0
  9. data/Pro-Changes.md +65 -2
  10. data/README.md +8 -9
  11. data/bin/sidekiq +5 -0
  12. data/bin/sidekiqctl +8 -2
  13. data/bin/sidekiqload +167 -0
  14. data/lib/sidekiq/api.rb +29 -31
  15. data/lib/sidekiq/cli.rb +41 -42
  16. data/lib/sidekiq/client.rb +5 -10
  17. data/lib/sidekiq/fetch.rb +35 -111
  18. data/lib/sidekiq/launcher.rb +102 -42
  19. data/lib/sidekiq/manager.rb +78 -180
  20. data/lib/sidekiq/middleware/server/logging.rb +10 -5
  21. data/lib/sidekiq/middleware/server/retry_jobs.rb +5 -5
  22. data/lib/sidekiq/processor.rb +126 -97
  23. data/lib/sidekiq/redis_connection.rb +23 -5
  24. data/lib/sidekiq/scheduled.rb +47 -26
  25. data/lib/sidekiq/testing.rb +96 -17
  26. data/lib/sidekiq/util.rb +20 -0
  27. data/lib/sidekiq/version.rb +1 -1
  28. data/lib/sidekiq/web.rb +17 -1
  29. data/lib/sidekiq/web_helpers.rb +26 -4
  30. data/lib/sidekiq/worker.rb +14 -0
  31. data/lib/sidekiq.rb +37 -14
  32. data/sidekiq.gemspec +11 -11
  33. data/test/helper.rb +45 -10
  34. data/test/test_actors.rb +137 -0
  35. data/test/test_api.rb +388 -388
  36. data/test/test_cli.rb +29 -59
  37. data/test/test_client.rb +60 -135
  38. data/test/test_extensions.rb +29 -23
  39. data/test/test_fetch.rb +2 -57
  40. data/test/test_launcher.rb +80 -0
  41. data/test/test_logging.rb +1 -1
  42. data/test/test_manager.rb +16 -131
  43. data/test/test_middleware.rb +3 -5
  44. data/test/test_processor.rb +110 -76
  45. data/test/test_rails.rb +21 -0
  46. data/test/test_redis_connection.rb +0 -1
  47. data/test/test_retry.rb +114 -162
  48. data/test/test_scheduled.rb +11 -17
  49. data/test/test_scheduling.rb +20 -42
  50. data/test/test_sidekiq.rb +46 -16
  51. data/test/test_testing.rb +80 -20
  52. data/test/test_testing_fake.rb +68 -8
  53. data/test/test_testing_inline.rb +3 -3
  54. data/test/test_util.rb +16 -0
  55. data/test/test_web.rb +17 -3
  56. data/test/test_web_helpers.rb +3 -2
  57. data/web/assets/images/favicon.ico +0 -0
  58. data/web/assets/javascripts/application.js +6 -1
  59. data/web/assets/javascripts/dashboard.js +2 -8
  60. data/web/assets/javascripts/locales/jquery.timeago.pt-br.js +14 -14
  61. data/web/assets/stylesheets/application.css +33 -56
  62. data/web/locales/de.yml +1 -1
  63. data/web/locales/en.yml +1 -0
  64. data/web/locales/{no.yml → nb.yml} +10 -2
  65. data/web/locales/uk.yml +76 -0
  66. data/web/views/_footer.erb +2 -7
  67. data/web/views/_job_info.erb +1 -1
  68. data/web/views/_nav.erb +2 -2
  69. data/web/views/_poll_js.erb +5 -0
  70. data/web/views/{_poll.erb → _poll_link.erb} +0 -3
  71. data/web/views/busy.erb +2 -1
  72. data/web/views/dead.erb +1 -0
  73. data/web/views/layout.erb +2 -0
  74. data/web/views/morgue.erb +3 -0
  75. data/web/views/queue.erb +1 -0
  76. data/web/views/queues.erb +1 -0
  77. data/web/views/retries.erb +3 -0
  78. data/web/views/retry.erb +1 -0
  79. data/web/views/scheduled.erb +1 -0
  80. data/web/views/scheduled_job_info.erb +1 -0
  81. metadata +81 -47
  82. data/lib/sidekiq/actor.rb +0 -39
  83. data/test/test_worker_generator.rb +0 -17
data/lib/sidekiq/util.rb CHANGED
@@ -19,6 +19,12 @@ module Sidekiq
19
19
  raise ex
20
20
  end
21
21
 
22
+ def safe_thread(name, &block)
23
+ Thread.new do
24
+ watchdog(name, &block)
25
+ end
26
+ end
27
+
22
28
  def logger
23
29
  Sidekiq.logger
24
30
  end
@@ -49,6 +55,20 @@ module Sidekiq
49
55
  handle_exception(ex, { event: event })
50
56
  end
51
57
  end
58
+ arr.clear
59
+ end
60
+
61
+ def want_a_hertz_donut?
62
+ # what's a hertz donut?
63
+ # punch! Hurts, don't it?
64
+ info = Sidekiq.redis {|c| c.info }
65
+ if info['connected_clients'].to_i > 1000 && info['hz'].to_i >= 10
66
+ Sidekiq.logger.warn { "Your Redis `hz` setting is too high at #{info['hz']}. See mperham/sidekiq#2431. Set it to 3 in #{info[:config_file]}" }
67
+ true
68
+ else
69
+ Sidekiq.logger.debug { "Redis hz: #{info['hz']}. Client count: #{info['connected_clients']}" }
70
+ false
71
+ end
52
72
  end
53
73
 
54
74
  end
@@ -1,3 +1,3 @@
1
1
  module Sidekiq
2
- VERSION = "3.4.1"
2
+ VERSION = "4.0.0"
3
3
  end
data/lib/sidekiq/web.rb CHANGED
@@ -11,6 +11,9 @@ module Sidekiq
11
11
  class Web < Sinatra::Base
12
12
  include Sidekiq::Paginator
13
13
 
14
+ enable :sessions
15
+ use Rack::Protection, :use => :authenticity_token unless ENV['RACK_ENV'] == 'test'
16
+
14
17
  set :root, File.expand_path(File.dirname(__FILE__) + "/../../web")
15
18
  set :public_folder, proc { "#{root}/assets" }
16
19
  set :views, proc { "#{root}/views" }
@@ -98,7 +101,7 @@ module Sidekiq
98
101
  end
99
102
 
100
103
  post '/morgue' do
101
- halt 404 unless params['key']
104
+ redirect request.path unless params['key']
102
105
 
103
106
  params['key'].each do |key|
104
107
  job = Sidekiq::DeadSet.new.fetch(*parse_params(key)).first
@@ -259,3 +262,16 @@ module Sidekiq
259
262
  end
260
263
  end
261
264
  end
265
+
266
+ if defined?(::ActionDispatch::Request::Session) &&
267
+ !::ActionDispatch::Request::Session.respond_to?(:each)
268
+ # mperham/sidekiq#2460
269
+ # Rack apps can't reuse the Rails session store without
270
+ # this monkeypatch
271
+ class ActionDispatch::Request::Session
272
+ def each(&block)
273
+ hash = self.to_hash
274
+ hash.each(&block)
275
+ end
276
+ end
277
+ end
@@ -45,7 +45,7 @@ module Sidekiq
45
45
  end
46
46
 
47
47
  def display_custom_head
48
- return unless @head_html
48
+ return unless defined?(@head_html)
49
49
  @head_html.map { |block| capture(&block) }.join
50
50
  end
51
51
 
@@ -160,7 +160,7 @@ module Sidekiq
160
160
  options = options.stringify_keys
161
161
  params.merge(options).map do |key, value|
162
162
  SAFE_QPARAMS.include?(key) ? "#{key}=#{value}" : next
163
- end.join("&")
163
+ end.compact.join("&")
164
164
  end
165
165
 
166
166
  def truncate(text, truncate_after_chars = 2000)
@@ -169,11 +169,26 @@ module Sidekiq
169
169
 
170
170
  def display_args(args, truncate_after_chars = 2000)
171
171
  args.map do |arg|
172
- a = arg.inspect
173
- h(truncate(a))
172
+ h(truncate(to_display(arg)))
174
173
  end.join(", ")
175
174
  end
176
175
 
176
+ def csrf_tag
177
+ "<input type='hidden' name='authenticity_token' value='#{session[:csrf]}'/>"
178
+ end
179
+
180
+ def to_display(arg)
181
+ begin
182
+ arg.inspect
183
+ rescue
184
+ begin
185
+ arg.to_s
186
+ rescue => ex
187
+ "Cannot display argument: [#{ex.class.name}] #{ex.message}"
188
+ end
189
+ end
190
+ end
191
+
177
192
  RETRY_JOB_KEYS = Set.new(%w(
178
193
  queue class args retry_count retried_at failed_at
179
194
  jid error_message error_class backtrace
@@ -230,5 +245,12 @@ module Sidekiq
230
245
  def product_version
231
246
  "Sidekiq v#{Sidekiq::VERSION}"
232
247
  end
248
+
249
+ def redis_connection_and_namespace
250
+ @redis_connection_and_namespace ||= begin
251
+ namespace_suffix = namespace == nil ? '' : "##{namespace}"
252
+ "#{redis_connection}#{namespace_suffix}"
253
+ end
254
+ end
233
255
  end
234
256
  end
@@ -24,6 +24,8 @@ module Sidekiq
24
24
  attr_accessor :jid
25
25
 
26
26
  def self.included(base)
27
+ raise ArgumentError, "You cannot include Sidekiq::Worker in an ActiveJob: #{base.name}" if base.ancestors.any? {|c| c.name == 'ActiveJob::Base' }
28
+
27
29
  base.extend(ClassMethods)
28
30
  base.class_attribute :sidekiq_options_hash
29
31
  base.class_attribute :sidekiq_retry_in_block
@@ -36,6 +38,18 @@ module Sidekiq
36
38
 
37
39
  module ClassMethods
38
40
 
41
+ def delay(*args)
42
+ raise ArgumentError, "Do not call .delay on a Sidekiq::Worker class, call .perform_async"
43
+ end
44
+
45
+ def delay_for(*args)
46
+ raise ArgumentError, "Do not call .delay_for on a Sidekiq::Worker class, call .perform_in"
47
+ end
48
+
49
+ def delay_until(*args)
50
+ raise ArgumentError, "Do not call .delay_until on a Sidekiq::Worker class, call .perform_at"
51
+ end
52
+
39
53
  def perform_async(*args)
40
54
  client_push('class' => self, 'args' => args)
41
55
  end
data/lib/sidekiq.rb CHANGED
@@ -76,9 +76,19 @@ module Sidekiq
76
76
  defined?(Sidekiq::CLI)
77
77
  end
78
78
 
79
- def self.redis(&block)
80
- raise ArgumentError, "requires a block" unless block
81
- redis_pool.with(&block)
79
+ def self.redis
80
+ raise ArgumentError, "requires a block" unless block_given?
81
+ redis_pool.with do |conn|
82
+ retryable = true
83
+ begin
84
+ yield conn
85
+ rescue Redis::CommandError => ex
86
+ #2550 Failover can cause the server to become a slave, need
87
+ # to disconnect and reopen the socket to get back to the master.
88
+ (conn.disconnect!; retryable = false; retry) if retryable && ex.message =~ /READONLY/
89
+ raise
90
+ end
91
+ end
82
92
  end
83
93
 
84
94
  def self.redis_pool
@@ -100,11 +110,25 @@ module Sidekiq
100
110
  end
101
111
 
102
112
  def self.server_middleware
103
- @server_chain ||= Processor.default_middleware
113
+ @server_chain ||= default_server_middleware
104
114
  yield @server_chain if block_given?
105
115
  @server_chain
106
116
  end
107
117
 
118
+ def self.default_server_middleware
119
+ require 'sidekiq/middleware/server/retry_jobs'
120
+ require 'sidekiq/middleware/server/logging'
121
+
122
+ Middleware::Chain.new do |m|
123
+ m.add Middleware::Server::Logging
124
+ m.add Middleware::Server::RetryJobs
125
+ if defined?(::ActiveRecord::Base)
126
+ require 'sidekiq/middleware/server/active_record'
127
+ m.add Sidekiq::Middleware::Server::ActiveRecord
128
+ end
129
+ end
130
+ end
131
+
108
132
  def self.default_worker_options=(hash)
109
133
  @default_worker_options = default_worker_options.merge(hash.stringify_keys)
110
134
  end
@@ -129,16 +153,6 @@ module Sidekiq
129
153
  Sidekiq::Logging.logger = log
130
154
  end
131
155
 
132
- # When set, overrides Sidekiq.options[:average_scheduled_poll_interval] and sets
133
- # the average interval that this process will delay before checking for
134
- # scheduled jobs or job retries that are ready to run.
135
- #
136
- # See sidekiq/scheduled.rb for an in-depth explanation of this value
137
- def self.poll_interval=(interval)
138
- $stderr.puts "DEPRECATION: `config.poll_interval = #{interval}` will be removed in Sidekiq 4. Please update to `config.average_scheduled_poll_interval = #{interval}`."
139
- self.options[:poll_interval_average] = interval
140
- end
141
-
142
156
  # How frequently Redis should be checked by a random Sidekiq process for
143
157
  # scheduled and retriable jobs. Each individual process will take turns by
144
158
  # waiting some multiple of this value.
@@ -172,6 +186,15 @@ module Sidekiq
172
186
  raise ArgumentError, "Invalid event name: #{event}" unless options[:lifecycle_events].key?(event)
173
187
  options[:lifecycle_events][event] << block
174
188
  end
189
+
190
+ # We are shutting down Sidekiq but what about workers that
191
+ # are working on some long job? This error is
192
+ # raised in workers that have not finished within the hard
193
+ # timeout limit. This is needed to rollback db transactions,
194
+ # otherwise Ruby's Thread#kill will commit. See #377.
195
+ # DO NOT RESCUE THIS ERROR IN YOUR WORKERS
196
+ class Shutdown < Interrupt; end
197
+
175
198
  end
176
199
 
177
200
  require 'sidekiq/extensions/class_methods'
data/sidekiq.gemspec CHANGED
@@ -4,7 +4,8 @@ require File.expand_path('../lib/sidekiq/version', __FILE__)
4
4
  Gem::Specification.new do |gem|
5
5
  gem.authors = ["Mike Perham"]
6
6
  gem.email = ["mperham@gmail.com"]
7
- gem.description = gem.summary = "Simple, efficient background processing for Ruby"
7
+ gem.summary = "Simple, efficient background processing for Ruby"
8
+ gem.description = "Simple, efficient background processing for Ruby."
8
9
  gem.homepage = "http://sidekiq.org"
9
10
  gem.license = "LGPL-3.0"
10
11
 
@@ -14,14 +15,13 @@ Gem::Specification.new do |gem|
14
15
  gem.name = "sidekiq"
15
16
  gem.require_paths = ["lib"]
16
17
  gem.version = Sidekiq::VERSION
17
- gem.add_dependency 'redis', '>= 3.0.6'
18
- gem.add_dependency 'redis-namespace', '>= 1.3.1'
19
- gem.add_dependency 'connection_pool', '>= 2.1.1'
20
- gem.add_dependency 'celluloid', '~> 0.16.0'
21
- gem.add_dependency 'json'
22
- gem.add_development_dependency 'sinatra'
23
- gem.add_development_dependency 'minitest', '~> 5.3.3'
24
- gem.add_development_dependency 'rake'
25
- gem.add_development_dependency 'rails', '~> 4.1.1'
26
- gem.add_development_dependency 'coveralls'
18
+ gem.add_dependency 'redis', '~> 3.2', '>= 3.2.1'
19
+ gem.add_dependency 'connection_pool', '~> 2.2', '>= 2.2.0'
20
+ gem.add_dependency 'json', '~> 1.0'
21
+ gem.add_dependency 'concurrent-ruby', '~> 1.0'
22
+ gem.add_development_dependency 'redis-namespace', '~> 1.5', '>= 1.5.2'
23
+ gem.add_development_dependency 'sinatra', '~> 1.4', '>= 1.4.6'
24
+ gem.add_development_dependency 'minitest', '~> 5.7', '>= 5.7.0'
25
+ gem.add_development_dependency 'rake', '~> 10.0'
26
+ gem.add_development_dependency 'rails', '~> 4', '>= 3.2.0'
27
27
  end
data/test/helper.rb CHANGED
@@ -1,18 +1,32 @@
1
1
  $TESTING = true
2
- require 'coveralls'
3
- Coveralls.wear! do
4
- add_filter "/test/"
5
- add_filter "/myapp/"
6
- end
2
+ # disable minitest/parallel threads
3
+ ENV["N"] = "0"
7
4
 
8
- ENV['RACK_ENV'] = ENV['RAILS_ENV'] = 'test'
9
- if ENV.has_key?("SIMPLECOV")
5
+ if ENV["COVERAGE"]
10
6
  require 'simplecov'
11
7
  SimpleCov.start do
12
8
  add_filter "/test/"
13
9
  add_filter "/myapp/"
14
10
  end
15
11
  end
12
+ ENV['RACK_ENV'] = ENV['RAILS_ENV'] = 'test'
13
+
14
+ trap 'USR1' do
15
+ threads = Thread.list
16
+
17
+ puts
18
+ puts "=" * 80
19
+ puts "Received USR1 signal; printing all #{threads.count} thread backtraces."
20
+
21
+ threads.each do |thr|
22
+ description = thr == Thread.main ? "Main thread" : thr.inspect
23
+ puts
24
+ puts "#{description} backtrace: "
25
+ puts thr.backtrace.join("\n")
26
+ end
27
+
28
+ puts "=" * 80
29
+ end
16
30
 
17
31
  begin
18
32
  require 'pry-byebug'
@@ -20,10 +34,7 @@ rescue LoadError
20
34
  end
21
35
 
22
36
  require 'minitest/autorun'
23
- require 'minitest/pride'
24
37
 
25
- require 'celluloid/test'
26
- Celluloid.boot
27
38
  require 'sidekiq'
28
39
  require 'sidekiq/util'
29
40
  Sidekiq.logger.level = Logger::ERROR
@@ -37,3 +48,27 @@ REDIS = Sidekiq::RedisConnection.create(:url => REDIS_URL, :namespace => 'testy'
37
48
  Sidekiq.configure_client do |config|
38
49
  config.redis = { :url => REDIS_URL, :namespace => 'testy' }
39
50
  end
51
+
52
+ def capture_logging(lvl=Logger::INFO)
53
+ old = Sidekiq.logger
54
+ begin
55
+ out = StringIO.new
56
+ logger = Logger.new(out)
57
+ logger.level = lvl
58
+ Sidekiq.logger = logger
59
+ yield
60
+ out.string
61
+ ensure
62
+ Sidekiq.logger = old
63
+ end
64
+ end
65
+
66
+ def with_logging(lvl=Logger::DEBUG)
67
+ old = Sidekiq.logger.level
68
+ begin
69
+ Sidekiq.logger.level = lvl
70
+ yield
71
+ ensure
72
+ Sidekiq.logger.level = old
73
+ end
74
+ end
@@ -0,0 +1,137 @@
1
+ require_relative 'helper'
2
+ require 'sidekiq/cli'
3
+ require 'sidekiq/fetch'
4
+ require 'sidekiq/scheduled'
5
+ require 'sidekiq/processor'
6
+
7
+ class TestActors < Sidekiq::Test
8
+ class JoeWorker
9
+ include Sidekiq::Worker
10
+ def perform(slp)
11
+ raise "boom" if slp == "boom"
12
+ sleep(slp) if slp > 0
13
+ $count += 1
14
+ end
15
+ end
16
+
17
+ describe 'threads' do
18
+ before do
19
+ Sidekiq.redis {|c| c.flushdb}
20
+ end
21
+
22
+ describe 'scheduler' do
23
+ it 'can start and stop' do
24
+ f = Sidekiq::Scheduled::Poller.new
25
+ f.start
26
+ f.terminate
27
+ end
28
+
29
+ it 'can schedule' do
30
+ ss = Sidekiq::ScheduledSet.new
31
+ q = Sidekiq::Queue.new
32
+
33
+ JoeWorker.perform_in(0.01, 0)
34
+
35
+ assert_equal 0, q.size
36
+ assert_equal 1, ss.size
37
+
38
+ sleep 0.015
39
+ s = Sidekiq::Scheduled::Poller.new
40
+ s.enqueue
41
+ assert_equal 1, q.size
42
+ assert_equal 0, ss.size
43
+ s.terminate
44
+ end
45
+ end
46
+
47
+ describe 'processor' do
48
+ before do
49
+ $count = 0
50
+ end
51
+
52
+ it 'can start and stop' do
53
+ f = Sidekiq::Processor.new(Mgr.new)
54
+ f.terminate
55
+ end
56
+
57
+ class Mgr
58
+ attr_reader :latest_error
59
+ attr_reader :mutex
60
+ attr_reader :cond
61
+ def initialize
62
+ @mutex = ::Mutex.new
63
+ @cond = ::ConditionVariable.new
64
+ end
65
+ def processor_died(inst, err)
66
+ @latest_error = err
67
+ @mutex.synchronize do
68
+ @cond.signal
69
+ end
70
+ end
71
+ def processor_stopped(inst)
72
+ @mutex.synchronize do
73
+ @cond.signal
74
+ end
75
+ end
76
+ def options
77
+ { :concurrency => 3, :queues => ['default'] }
78
+ end
79
+ end
80
+
81
+ it 'can process' do
82
+ mgr = Mgr.new
83
+
84
+ p = Sidekiq::Processor.new(mgr)
85
+ JoeWorker.perform_async(0)
86
+
87
+ a = $count
88
+ p.process_one
89
+ b = $count
90
+ assert_equal a + 1, b
91
+ end
92
+
93
+ it 'deals with errors' do
94
+ mgr = Mgr.new
95
+
96
+ p = Sidekiq::Processor.new(mgr)
97
+ JoeWorker.perform_async("boom")
98
+ q = Sidekiq::Queue.new
99
+ assert_equal 1, q.size
100
+
101
+ a = $count
102
+ mgr.mutex.synchronize do
103
+ p.start
104
+ mgr.cond.wait(mgr.mutex)
105
+ end
106
+ b = $count
107
+ assert_equal a, b
108
+
109
+ sleep 0.001
110
+ assert_equal false, p.thread.status
111
+ p.terminate(true)
112
+ refute_nil mgr.latest_error
113
+ assert_equal RuntimeError, mgr.latest_error.class
114
+ end
115
+
116
+ it 'gracefully kills' do
117
+ mgr = Mgr.new
118
+
119
+ p = Sidekiq::Processor.new(mgr)
120
+ JoeWorker.perform_async(1)
121
+ q = Sidekiq::Queue.new
122
+ assert_equal 1, q.size
123
+
124
+ a = $count
125
+ p.start
126
+ sleep(0.02)
127
+ p.terminate
128
+ p.kill(true)
129
+
130
+ b = $count
131
+ assert_equal a, b
132
+ assert_equal false, p.thread.status
133
+ refute mgr.latest_error, mgr.latest_error.to_s
134
+ end
135
+ end
136
+ end
137
+ end