sidekiq 0.8.0 → 0.9.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 (66) hide show
  1. data/Changes.md +11 -0
  2. data/Gemfile +7 -1
  3. data/LICENSE +7 -1
  4. data/README.md +2 -0
  5. data/TODO.md +1 -2
  6. data/bin/sidekiq +1 -1
  7. data/config.ru +8 -0
  8. data/examples/config.yml +0 -2
  9. data/examples/scheduling.rb +37 -0
  10. data/lib/sidekiq.rb +25 -11
  11. data/lib/sidekiq/capistrano.rb +8 -5
  12. data/lib/sidekiq/cli.rb +20 -21
  13. data/lib/sidekiq/client.rb +9 -6
  14. data/lib/sidekiq/extensions/action_mailer.rb +1 -2
  15. data/lib/sidekiq/extensions/active_record.rb +1 -3
  16. data/lib/sidekiq/extensions/generic_proxy.rb +1 -1
  17. data/lib/sidekiq/manager.rb +28 -22
  18. data/lib/sidekiq/middleware/client/unique_jobs.rb +3 -3
  19. data/lib/sidekiq/middleware/server/failure_jobs.rb +1 -1
  20. data/lib/sidekiq/middleware/server/unique_jobs.rb +1 -1
  21. data/lib/sidekiq/processor.rb +4 -6
  22. data/lib/sidekiq/rails.rb +16 -1
  23. data/lib/sidekiq/redis_connection.rb +3 -3
  24. data/lib/sidekiq/util.rb +3 -15
  25. data/lib/sidekiq/version.rb +1 -1
  26. data/lib/sidekiq/web.rb +93 -0
  27. data/lib/sidekiq/worker.rb +0 -4
  28. data/myapp/Gemfile +15 -3
  29. data/myapp/app/controllers/work_controller.rb +1 -1
  30. data/myapp/config/initializers/sidekiq.rb +2 -2
  31. data/myapp/config/routes.rb +2 -2
  32. data/sidekiq.gemspec +3 -1
  33. data/test/helper.rb +4 -0
  34. data/test/test_cli.rb +26 -14
  35. data/test/test_client.rb +2 -2
  36. data/test/test_extensions.rb +11 -7
  37. data/test/test_manager.rb +2 -3
  38. data/test/test_middleware.rb +1 -1
  39. data/test/test_stats.rb +28 -29
  40. data/test/test_testing.rb +47 -14
  41. data/test/test_web.rb +51 -0
  42. data/web/assets/images/bootstrap/glyphicons-halflings-white.png +0 -0
  43. data/web/assets/images/bootstrap/glyphicons-halflings.png +0 -0
  44. data/web/assets/javascripts/application.js +3 -0
  45. data/web/assets/javascripts/vendor/bootstrap.js +12 -0
  46. data/web/assets/javascripts/vendor/bootstrap/bootstrap-alert.js +91 -0
  47. data/web/assets/javascripts/vendor/bootstrap/bootstrap-button.js +98 -0
  48. data/web/assets/javascripts/vendor/bootstrap/bootstrap-carousel.js +154 -0
  49. data/web/assets/javascripts/vendor/bootstrap/bootstrap-collapse.js +136 -0
  50. data/web/assets/javascripts/vendor/bootstrap/bootstrap-dropdown.js +92 -0
  51. data/web/assets/javascripts/vendor/bootstrap/bootstrap-modal.js +210 -0
  52. data/web/assets/javascripts/vendor/bootstrap/bootstrap-popover.js +95 -0
  53. data/web/assets/javascripts/vendor/bootstrap/bootstrap-scrollspy.js +125 -0
  54. data/web/assets/javascripts/vendor/bootstrap/bootstrap-tab.js +130 -0
  55. data/web/assets/javascripts/vendor/bootstrap/bootstrap-tooltip.js +270 -0
  56. data/web/assets/javascripts/vendor/bootstrap/bootstrap-transition.js +51 -0
  57. data/web/assets/javascripts/vendor/bootstrap/bootstrap-typeahead.js +271 -0
  58. data/web/assets/javascripts/vendor/jquery.js +9266 -0
  59. data/web/assets/stylesheets/application.css +11 -0
  60. data/web/assets/stylesheets/vendor/bootstrap-responsive.css +567 -0
  61. data/web/assets/stylesheets/vendor/bootstrap.css +3365 -0
  62. data/web/views/index.slim +42 -0
  63. data/web/views/layout.slim +24 -0
  64. data/web/views/queue.slim +11 -0
  65. metadata +71 -22
  66. data/lib/sidekiq/middleware/client/resque_web_compatibility.rb +0 -14
data/Changes.md CHANGED
@@ -1,3 +1,14 @@
1
+ 0.9.0
2
+ -----------
3
+
4
+ - Add -t option to configure the TERM shutdown timeout
5
+ - TERM shutdown timeout is now configurable, defaults to 5 seconds.
6
+ - USR1 signal now stops Sidekiq from accepting new work,
7
+ capistrano sends USR1 at start of deploy and TERM at end of deploy
8
+ giving workers the maximum amount of time to finish.
9
+ - New Sidekiq::Web rack application available
10
+ - Updated Sidekiq.redis API
11
+
1
12
  0.8.0
2
13
  -----------
3
14
 
data/Gemfile CHANGED
@@ -1,4 +1,10 @@
1
1
  source 'http://rubygems.org'
2
2
  gemspec
3
3
 
4
- gem 'simplecov', :require => false, :group => :test
4
+ gem 'slim'
5
+ gem 'sprockets'
6
+ gem 'sass'
7
+
8
+ group :test do
9
+ gem 'simplecov', :require => false
10
+ end
data/LICENSE CHANGED
@@ -12,5 +12,11 @@ You may choose to license Sidekiq in one of two ways:
12
12
  $50 to my Pledgie fund at http://www.pledgie.com/campaigns/16623 to use
13
13
  this license.
14
14
 
15
- This commercial license is just meant to help support my OSS work. If
15
+ The commercial license is just meant to help support my OSS work. If
16
16
  you do buy the commercial licensing option, thank you for your support!
17
+
18
+ The commercial license is meant to be per-site. Consulting shops must buy
19
+ one license per client engagement which uses Sidekiq. For instance,
20
+ GitHub could license Sidekiq for $50 for their own internal use but they
21
+ must pay $50 per GitHub::FI install because FI is a separate installation
22
+ from the GitHub website managed by the FI customer.
data/README.md CHANGED
@@ -42,6 +42,8 @@ More Information
42
42
  -----------------
43
43
 
44
44
  Please see the [sidekiq wiki](https://github.com/mperham/sidekiq/wiki) for more information.
45
+ [#sidekiq on irc.freenode.net](irc://irc.freenode.net/#sidekiq) is dedicated to this project,
46
+ but bug reports or feature requests suggestions should still go through [issues on Github](https://github.com/mperham/sidekiq/issues).
45
47
 
46
48
 
47
49
  License
data/TODO.md CHANGED
@@ -1,2 +1 @@
1
- - resque-ui-esque web ui
2
- - scheduled tasks [GH-66]
1
+ - reschedule failed tasks
@@ -10,7 +10,7 @@ rescue LoadError
10
10
  end
11
11
 
12
12
  begin
13
- cli = Sidekiq::CLI.new
13
+ cli = Sidekiq::CLI.instance
14
14
  cli.parse
15
15
  cli.run
16
16
  rescue => e
@@ -0,0 +1,8 @@
1
+ require 'sidekiq'
2
+
3
+ Sidekiq.configure_client do |config|
4
+ config.redis = { :size => 1 }
5
+ end
6
+
7
+ require 'sidekiq/web'
8
+ run Sidekiq::Web
@@ -3,8 +3,6 @@
3
3
  # sidekiq -C config.yml
4
4
  ---
5
5
  :verbose: false
6
- :namespace: test_namespace
7
- :server: 127.0.0.1:1234
8
6
  :concurrency: 25
9
7
  :queues:
10
8
  - [often, 2]
@@ -0,0 +1,37 @@
1
+ # Sidekiq defers scheduling to other, better suited gems.
2
+ # If you want to run a job regularly, here's an example
3
+ # of using the 'whenever' gem to push jobs to Sidekiq
4
+ # regularly.
5
+
6
+ class MyWorker
7
+ include Sidekiq::Worker
8
+
9
+ def perform(count)
10
+ puts "Job ##{count}: Late night, so tired..."
11
+ end
12
+
13
+ def self.late_night_work
14
+ 10.times do |x|
15
+ perform_async(x)
16
+ end
17
+ end
18
+ end
19
+
20
+ # Kick off a bunch of jobs early in the morning
21
+ every 1.day, :at => '4:30 am' do
22
+ runner "MyWorker.late_night_work"
23
+ end
24
+
25
+
26
+ class HourlyWorker
27
+ include Sidekiq::Worker
28
+
29
+ def perform
30
+ cleanup_database
31
+ format_hard_drive
32
+ end
33
+ end
34
+
35
+ every :hour do # Many shortcuts available: :hour, :day, :month, :year, :reboot
36
+ runner "HourlyWorker.perform_async"
37
+ end
@@ -1,11 +1,11 @@
1
1
  require 'sidekiq/version'
2
2
  require 'sidekiq/client'
3
3
  require 'sidekiq/worker'
4
- require 'sidekiq/rails' if defined?(::Rails)
4
+ require 'sidekiq/rails'
5
5
  require 'sidekiq/redis_connection'
6
6
 
7
- require 'sidekiq/extensions/action_mailer' if defined?(::ActionMailer)
8
- require 'sidekiq/extensions/active_record' if defined?(::ActiveRecord)
7
+ require 'sidekiq/extensions/action_mailer'
8
+ require 'sidekiq/extensions/active_record'
9
9
 
10
10
  module Sidekiq
11
11
 
@@ -14,6 +14,7 @@ module Sidekiq
14
14
  :concurrency => 25,
15
15
  :require => '.',
16
16
  :environment => nil,
17
+ :timeout => 5,
17
18
  }
18
19
 
19
20
  def self.options
@@ -28,7 +29,7 @@ module Sidekiq
28
29
  # Configuration for Sidekiq server, use like:
29
30
  #
30
31
  # Sidekiq.configure_server do |config|
31
- # config.redis = Sidekiq::RedisConnection.create(:namespace => 'myapp', :size => 25, :url => 'redis://myhost:8877/mydb')
32
+ # config.redis = { :namespace => 'myapp', :size => 25, :url => 'redis://myhost:8877/mydb' }
32
33
  # config.server_middleware do |chain|
33
34
  # chain.add MyServerHook
34
35
  # end
@@ -41,7 +42,7 @@ module Sidekiq
41
42
  # Configuration for Sidekiq client, use like:
42
43
  #
43
44
  # Sidekiq.configure_client do |config|
44
- # config.redis = Sidekiq::RedisConnection.create(:namespace => 'myapp', :size => 1, :url => 'redis://myhost:8877/mydb')
45
+ # config.redis = { :namespace => 'myapp', :size => 1, :url => 'redis://myhost:8877/mydb' }
45
46
  # end
46
47
  def self.configure_client
47
48
  yield self unless server?
@@ -51,21 +52,34 @@ module Sidekiq
51
52
  defined?(Sidekiq::CLI)
52
53
  end
53
54
 
54
- def self.redis
55
+ def self.redis(&block)
55
56
  @redis ||= Sidekiq::RedisConnection.create
57
+ if block_given?
58
+ @redis.with(&block)
59
+ else
60
+ Sidekiq::Util.logger.info "*****************************************************
61
+ Sidekiq.redis now takes a block:
62
+
63
+ Sidekiq.redis { |connection| connection.smembers('myset') }
64
+
65
+ Please update your code accordingly.
66
+ Called from #{caller[0]}
67
+ *****************************************************"
68
+ @redis
69
+ end
56
70
  end
57
71
 
58
72
  def self.redis=(hash)
59
- if !hash.is_a?(Hash)
60
- puts "*****************************************************
73
+ @redis = if hash.is_a?(Hash)
74
+ RedisConnection.create(hash)
75
+ else
76
+ Sidekiq::Util.logger.info "*****************************************************
61
77
  Sidekiq.redis now takes a Hash:
62
78
  old: Sidekiq.redis = Sidekiq::RedisConnection.create(:url => 'redis://foo.com', :namespace => 'abc', :size => 12)
63
79
  new: Sidekiq.redis = { :url => 'redis://foo.com', :namespace => 'xyz', :size => 12 }
64
80
  Called from #{caller[0]}
65
81
  *****************************************************"
66
- @redis = hash
67
- else
68
- @redis = RedisConnection.create(hash)
82
+ hash
69
83
  end
70
84
  end
71
85
 
@@ -1,22 +1,25 @@
1
1
  Capistrano::Configuration.instance.load do
2
+ before "deploy", "sidekiq:quiet"
2
3
  after "deploy", "sidekiq:restart"
3
4
 
5
+ _cset(:sidekiq_timeout) { 5 }
6
+
4
7
  namespace :sidekiq do
5
8
 
6
- desc "Force stop sidekiq"
7
- task :kill do
8
- run "cd #{current_path} && kill `cat tmp/pids/sidekiq.pid` && sleep 5 && kill -9 `cat tmp/pids/sidekiq.pid`"
9
+ desc "Quiet sidekiq (stop accepting new work)"
10
+ task :quiet do
11
+ run "cd #{current_path} && kill -USR1 `cat #{current_path}/tmp/pids/sidekiq.pid`"
9
12
  end
10
13
 
11
14
  desc "Stop sidekiq"
12
15
  task :stop do
13
- run "cd #{current_path} && kill `cat tmp/pids/sidekiq.pid`"
16
+ run "cd #{current_path} && kill `cat #{current_path}/tmp/pids/sidekiq.pid` && sleep #{fetch :sidekiq_timeout} && kill -9 `cat #{current_path}/tmp/pids/sidekiq.pid` ; rm #{current_path}/tmp/pids/sidekiq.pid"
14
17
  end
15
18
 
16
19
  desc "Start sidekiq"
17
20
  task :start do
18
21
  rails_env = fetch(:rails_env, "production")
19
- run "cd #{current_path} && nohup bundle exec sidekiq -e #{rails_env} -C config/sidekiq.yml -P tmp/pids/sidekiq.pid &>> log/sidekiq.log &"
22
+ run "cd #{current_path} && nohup bundle exec sidekiq -e #{rails_env} -C #{current_path}/config/sidekiq.yml -P #{current_path}/tmp/pids/sidekiq.pid >> #{current_path}/log/sidekiq.log 2>&1 &"
20
23
  end
21
24
 
22
25
  desc "Restart sidekiq"
@@ -9,7 +9,14 @@ trap 'TERM' do
9
9
  Thread.main.raise Interrupt
10
10
  end
11
11
 
12
+ trap 'USR1' do
13
+ Sidekiq::Util.logger.info "Received USR1, no longer accepting new work"
14
+ mgr = Sidekiq::CLI.instance.manager
15
+ mgr.stop! if mgr
16
+ end
17
+
12
18
  require 'yaml'
19
+ require 'singleton'
13
20
  require 'optparse'
14
21
  require 'sidekiq'
15
22
  require 'sidekiq/util'
@@ -18,40 +25,40 @@ require 'sidekiq/manager'
18
25
  module Sidekiq
19
26
  class CLI
20
27
  include Util
28
+ include Singleton
21
29
 
22
30
  # Used for CLI testing
23
- attr_accessor :code
31
+ attr_accessor :code, :manager
24
32
 
25
33
  def initialize
26
34
  @code = nil
35
+ @manager = nil
27
36
  end
28
37
 
29
38
  def parse(args=ARGV)
39
+ @code = nil
30
40
  Sidekiq::Util.logger
31
41
 
32
42
  cli = parse_options(args)
33
43
  config = parse_config(cli)
34
44
  options.merge!(config.merge(cli))
35
45
 
36
- set_logger_level_to_debug if options[:verbose]
46
+ Sidekiq::Util.logger.level = Logger::DEBUG if options[:verbose]
37
47
 
38
- write_pid
39
48
  validate!
49
+ write_pid
40
50
  boot_system
41
51
  end
42
52
 
43
53
  def run
44
- manager = Sidekiq::Manager.new(options)
54
+ @manager = Sidekiq::Manager.new(options)
45
55
  begin
46
56
  logger.info 'Starting processing, hit Ctrl-C to stop'
47
57
  manager.start!
48
- # HACK need to determine how to pause main thread while
49
- # waiting for signals.
50
58
  sleep
51
59
  rescue Interrupt
52
- # TODO Need clean shutdown support from Celluloid
53
60
  logger.info 'Shutting down'
54
- manager.stop!
61
+ manager.stop!(:shutdown => true, :timeout => options[:timeout])
55
62
  manager.wait(:shutdown)
56
63
  end
57
64
  end
@@ -108,21 +115,17 @@ module Sidekiq
108
115
  end
109
116
 
110
117
  o.on "-v", "--verbose", "Print more verbose output" do
111
- set_logger_level_to_debug
112
- end
113
-
114
- o.on "-n", "--namespace NAMESPACE", "namespace worker queues are under (DEPRECATED)" do |arg|
115
- puts "Namespace support has been removed, see https://github.com/mperham/sidekiq/issues/61"
116
- end
117
-
118
- o.on "-s", "--server LOCATION", "Where to find Redis (DEPRECATED)" do |arg|
119
- puts "Server location support has been removed, see https://github.com/mperham/sidekiq/issues/61"
118
+ Sidekiq::Util.logger.level = Logger::DEBUG
120
119
  end
121
120
 
122
121
  o.on '-e', '--environment ENV', "Application environment" do |arg|
123
122
  opts[:environment] = arg
124
123
  end
125
124
 
125
+ o.on '-t', '--timeout NUM', "Shutdown timeout" do |arg|
126
+ opts[:timeout] = arg.to_i
127
+ end
128
+
126
129
  o.on '-r', '--require [PATH|DIR]', "Location of Rails application with workers or file to require" do |arg|
127
130
  opts[:require] = arg
128
131
  end
@@ -173,9 +176,5 @@ module Sidekiq
173
176
  end
174
177
  end
175
178
 
176
- def set_logger_level_to_debug
177
- Sidekiq::Util.logger.level = Logger::DEBUG
178
- end
179
-
180
179
  end
181
180
  end
@@ -1,9 +1,7 @@
1
1
  require 'multi_json'
2
2
  require 'redis'
3
3
 
4
- require 'sidekiq/redis_connection'
5
4
  require 'sidekiq/middleware/chain'
6
- require 'sidekiq/middleware/client/resque_web_compatibility'
7
5
  require 'sidekiq/middleware/client/unique_jobs'
8
6
 
9
7
  module Sidekiq
@@ -16,16 +14,15 @@ module Sidekiq
16
14
  def self.default_middleware
17
15
  Middleware::Chain.new do |m|
18
16
  m.add Middleware::Client::UniqueJobs
19
- m.add Middleware::Client::ResqueWebCompatibility
20
17
  end
21
18
  end
22
19
 
23
20
  def self.registered_workers
24
- Sidekiq.redis.smembers('workers')
21
+ Sidekiq.redis { |x| x.smembers('workers') }
25
22
  end
26
23
 
27
24
  def self.registered_queues
28
- Sidekiq.redis.smembers('queues')
25
+ Sidekiq.redis { |x| x.smembers('queues') }
29
26
  end
30
27
 
31
28
  def self.queue_mappings
@@ -44,7 +41,13 @@ module Sidekiq
44
41
 
45
42
  pushed = false
46
43
  Sidekiq.client_middleware.invoke(item, queue) do
47
- Sidekiq.redis.rpush("queue:#{queue}", MultiJson.encode(item))
44
+ payload = MultiJson.encode(item)
45
+ Sidekiq.redis do |conn|
46
+ conn.multi do
47
+ conn.sadd('queues', queue)
48
+ conn.rpush("queue:#{queue}", payload)
49
+ end
50
+ end
48
51
  pushed = true
49
52
  end
50
53
  pushed
@@ -22,6 +22,5 @@ module Sidekiq
22
22
  end
23
23
  end
24
24
 
25
- ::ActionMailer::Base.extend(ActionMailer)
26
25
  end
27
- end if defined?(::ActionMailer)
26
+ end
@@ -23,7 +23,5 @@ module Sidekiq
23
23
  end
24
24
  end
25
25
 
26
- ::ActiveRecord::Base.extend(ActiveRecord)
27
- ::ActiveRecord::Base.send(:include, ActiveRecord)
28
26
  end
29
- end if defined?(::ActiveRecord)
27
+ end
@@ -13,7 +13,7 @@ module Sidekiq
13
13
  # to JSON and then deserialized on the other side back into a
14
14
  # Ruby object.
15
15
  obj = [@target, name, args]
16
- ::Sidekiq::Client.push('class' => @performable.name, 'args' => [::YAML.dump(obj)])
16
+ @performable.perform_async(::YAML.dump(obj))
17
17
  end
18
18
  end
19
19
 
@@ -4,6 +4,7 @@ require 'multi_json'
4
4
 
5
5
  require 'sidekiq/util'
6
6
  require 'sidekiq/processor'
7
+ require 'connection_pool/version'
7
8
 
8
9
  module Sidekiq
9
10
 
@@ -19,7 +20,8 @@ module Sidekiq
19
20
  trap_exit :processor_died
20
21
 
21
22
  def initialize(options={})
22
- logger.info "Booting sidekiq #{Sidekiq::VERSION} with Redis at #{redis.client.location}"
23
+ logger.info "Booting sidekiq #{Sidekiq::VERSION} with Redis at #{redis {|x| x.client.location}}"
24
+ logger.info "Running in #{RUBY_DESCRIPTION}"
23
25
  logger.debug { options.inspect }
24
26
  @count = options[:concurrency] || 25
25
27
  @queues = options[:queues]
@@ -30,33 +32,33 @@ module Sidekiq
30
32
  @ready = @count.times.map { Processor.new_link(current_actor) }
31
33
  end
32
34
 
33
- def stop
35
+ def stop(options={})
36
+ shutdown = options[:shutdown]
37
+ timeout = options[:timeout]
38
+
34
39
  @done = true
35
- @ready.each(&:terminate)
40
+ @ready.each { |x| x.terminate if x.alive? }
36
41
  @ready.clear
37
42
 
38
- redis.with_connection do |conn|
43
+ redis do |conn|
39
44
  workers = conn.smembers('workers')
40
45
  workers.each do |name|
41
46
  conn.srem('workers', name) if name =~ /:#{process_id}-/
42
47
  end
43
48
  end
44
49
 
45
- if @busy.empty?
46
- return signal(:shutdown)
47
- end
50
+ if shutdown
51
+ if @busy.empty?
52
+ # after(0) needed to avoid deadlock in Celluoid after USR1 + TERM
53
+ return after(0) { signal(:shutdown) }
54
+ else
55
+ logger.info { "Pausing #{timeout} seconds to allow workers to finish..." }
56
+ end
48
57
 
49
- logger.info("Pausing 5 seconds to allow workers to finish...")
50
- after(5) do
51
- @busy.each(&:terminate)
52
- redis.with_connection do |conn|
53
- conn.multi do
54
- @busy.each do |busy|
55
- conn.lpush("queue:#{busy.queue}", busy.msg)
56
- end
57
- end
58
+ after(timeout) do
59
+ @busy.each { |x| x.terminate if x.alive? }
60
+ signal(:shutdown)
58
61
  end
59
- signal(:shutdown)
60
62
  end
61
63
  end
62
64
 
@@ -72,11 +74,11 @@ module Sidekiq
72
74
  watchdog('sidekiq processor_done crashed!') do
73
75
  @done_callback.call(processor) if @done_callback
74
76
  @busy.delete(processor)
75
- processor.msg = processor.queue = nil
76
77
  if stopped?
77
78
  processor.terminate if processor.alive?
79
+ signal(:shutdown) if @busy.empty?
78
80
  else
79
- @ready << processor
81
+ @ready << processor if processor.alive?
80
82
  end
81
83
  dispatch
82
84
  end
@@ -88,18 +90,18 @@ module Sidekiq
88
90
  unless stopped?
89
91
  @ready << Processor.new_link(current_actor)
90
92
  dispatch
93
+ else
94
+ signal(:shutdown) if @busy.empty?
91
95
  end
92
96
  end
93
97
 
94
98
  private
95
99
 
96
100
  def find_work(queue)
97
- msg = redis.lpop("queue:#{queue}")
101
+ msg = redis { |x| x.lpop("queue:#{queue}") }
98
102
  if msg
99
103
  processor = @ready.pop
100
104
  @busy << processor
101
- processor.msg = msg
102
- processor.queue = queue
103
105
  processor.process!(MultiJson.decode(msg), queue)
104
106
  end
105
107
  !!msg
@@ -109,6 +111,10 @@ module Sidekiq
109
111
  watchdog("Fatal error in sidekiq, dispatch loop died") do
110
112
  return if stopped?
111
113
 
114
+ # This is a safety check to ensure we haven't leaked
115
+ # processors somehow.
116
+ raise "BUG: No processors, cannot continue!" if @ready.empty? && @busy.empty?
117
+
112
118
  # Dispatch loop
113
119
  loop do
114
120
  break logger.debug('no processors') if @ready.empty?