sidekiq 2.14.1 → 2.15.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 (54) hide show
  1. data/Changes.md +24 -0
  2. data/README.md +6 -3
  3. data/lib/sidekiq.rb +8 -0
  4. data/lib/sidekiq/api.rb +1 -1
  5. data/lib/sidekiq/cli.rb +1 -1
  6. data/lib/sidekiq/client.rb +24 -2
  7. data/lib/sidekiq/exception_handler.rb +2 -2
  8. data/lib/sidekiq/launcher.rb +38 -9
  9. data/lib/sidekiq/manager.rb +0 -3
  10. data/lib/sidekiq/testing.rb +65 -4
  11. data/lib/sidekiq/testing/inline.rb +27 -40
  12. data/lib/sidekiq/util.rb +1 -0
  13. data/lib/sidekiq/version.rb +1 -1
  14. data/lib/sidekiq/web.rb +16 -3
  15. data/lib/sidekiq/worker.rb +6 -8
  16. data/sidekiq.gemspec +4 -4
  17. data/test/helper.rb +8 -0
  18. data/test/test_api.rb +1 -1
  19. data/test/test_cli.rb +1 -1
  20. data/test/test_client.rb +23 -2
  21. data/test/test_exception_handler.rb +1 -1
  22. data/test/test_extensions.rb +1 -1
  23. data/test/test_fetch.rb +1 -1
  24. data/test/test_manager.rb +11 -18
  25. data/test/test_middleware.rb +1 -1
  26. data/test/test_processor.rb +1 -1
  27. data/test/test_redis_connection.rb +3 -3
  28. data/test/test_retry.rb +1 -1
  29. data/test/test_scheduled.rb +1 -1
  30. data/test/test_scheduling.rb +10 -2
  31. data/test/test_sidekiq.rb +2 -2
  32. data/test/test_testing.rb +43 -230
  33. data/test/test_testing_fake.rb +265 -0
  34. data/test/test_testing_inline.rb +4 -7
  35. data/test/test_util.rb +1 -1
  36. data/test/test_web.rb +1 -1
  37. data/web/assets/javascripts/dashboard.js +3 -1
  38. data/web/assets/stylesheets/application.css +84 -21
  39. data/web/assets/stylesheets/bootstrap.css +4 -15
  40. data/web/views/_nav.erb +12 -2
  41. data/web/views/_poll.erb +14 -0
  42. data/web/views/_status.erb +1 -1
  43. data/web/views/_summary.erb +23 -15
  44. data/web/views/_workers.erb +1 -1
  45. data/web/views/dashboard.erb +37 -34
  46. data/web/views/index.erb +2 -2
  47. data/web/views/layout.erb +3 -23
  48. data/web/views/queue.erb +4 -4
  49. data/web/views/queues.erb +1 -1
  50. data/web/views/retries.erb +10 -6
  51. data/web/views/retry.erb +1 -1
  52. data/web/views/scheduled.erb +2 -2
  53. data/web/views/scheduled_job_info.erb +1 -1
  54. metadata +13 -10
data/Changes.md CHANGED
@@ -1,3 +1,27 @@
1
+ 2.15.0
2
+ -----------
3
+
4
+ - The Core Sidekiq actors are now monitored. If any crash, the
5
+ Sidekiq process logs the error and exits immediately. This is to
6
+ help prevent "stuck" Sidekiq processes which are running but don't
7
+ appear to be doing any work. [#1194]
8
+ - Sidekiq's testing behavior is now dynamic. You can choose between
9
+ `inline` and `fake` behavior in your tests. See
10
+ [Testing](wiki/Testing) for detail. [#1193]
11
+ - The Retries table has a new column for the error message.
12
+ - The Web UI topbar now contains the status and live poll button.
13
+ - Orphaned worker records are now auto-vacuumed when you vist the
14
+ Workers page in the Web UI.
15
+ - Sidekiq.default\_worker\_options allows you to configure default
16
+ options for all Sidekiq worker types.
17
+
18
+ ```ruby
19
+ Sidekiq.default_worker_options = { 'queue' => 'default', 'backtrace' => true }
20
+ ```
21
+ - Added two Sidekiq::Client class methods for compatibility with resque-scheduler:
22
+ `enqueue_to_in` and `enqueue_in` [#1212]
23
+ - Upgrade Web UI to Bootstrap 3.0. [#1211, jeffboek]
24
+
1
25
  2.14.1
2
26
  -----------
3
27
 
data/README.md CHANGED
@@ -8,7 +8,7 @@ Simple, efficient background processing for Ruby.
8
8
 
9
9
  Sidekiq uses threads to handle many jobs at the same time in the
10
10
  same process. It does not require Rails but will integrate tightly with
11
- Rails 3 to make background processing dead simple.
11
+ Rails 3/4 to make background processing dead simple.
12
12
 
13
13
  Sidekiq is compatible with Resque. It uses the exact same
14
14
  message format as Resque so it can integrate into an existing Resque processing farm.
@@ -24,8 +24,11 @@ the same CPU and perform the same amount of work.
24
24
  Requirements
25
25
  -----------------
26
26
 
27
- I test on Ruby 1.9.3 and JRuby 1.7.x. Other versions/VMs are
28
- untested but I will do my best to support them. Ruby 1.8 is not supported.
27
+ I test with the latest Ruby (2.0) and JRuby versions (1.7). Other versions/VMs
28
+ are untested but might work fine.
29
+
30
+ The last two major Rails releases (3.2 and 4.0) are officially supported, other
31
+ versions might work fine.
29
32
 
30
33
  Redis 2.4 or greater is required.
31
34
 
@@ -89,6 +89,14 @@ module Sidekiq
89
89
  @server_chain
90
90
  end
91
91
 
92
+ def self.default_worker_options=(hash)
93
+ @default_worker_options = default_worker_options.merge(hash)
94
+ end
95
+
96
+ def self.default_worker_options
97
+ @default_worker_options || { 'retry' => true, 'queue' => 'default' }
98
+ end
99
+
92
100
  def self.load_json(string)
93
101
  JSON.parse(string)
94
102
  end
@@ -260,7 +260,7 @@ module Sidekiq
260
260
 
261
261
  def schedule(timestamp, message)
262
262
  Sidekiq.redis do |conn|
263
- conn.zadd(@zset, timestamp.to_s, Sidekiq.dump_json(message))
263
+ conn.zadd(@zset, timestamp.to_f.to_s, Sidekiq.dump_json(message))
264
264
  end
265
265
  end
266
266
 
@@ -220,7 +220,7 @@ module Sidekiq
220
220
  if !File.exist?(options[:require]) ||
221
221
  (File.directory?(options[:require]) && !File.exist?("#{options[:require]}/config/application.rb"))
222
222
  logger.info "=================================================================="
223
- logger.info " Please point sidekiq to a Rails 3 application or a Ruby file "
223
+ logger.info " Please point sidekiq to a Rails 3/4 application or a Ruby file "
224
224
  logger.info " to load your worker classes with -r [DIR|FILE]."
225
225
  logger.info "=================================================================="
226
226
  logger.info @parser
@@ -70,7 +70,8 @@ module Sidekiq
70
70
  pushed ? payloads.size : nil
71
71
  end
72
72
 
73
- # Resque compatibility helpers.
73
+ # Resque compatibility helpers. Note all helpers
74
+ # should go through Worker#client_push.
74
75
  #
75
76
  # Example usage:
76
77
  # Sidekiq::Client.enqueue(MyWorker, 'foo', 1, :bat => 'bar')
@@ -88,6 +89,27 @@ module Sidekiq
88
89
  klass.client_push('queue' => queue, 'class' => klass, 'args' => args)
89
90
  end
90
91
 
92
+ # Example usage:
93
+ # Sidekiq::Client.enqueue_to_in(:queue_name, 3.minutes, MyWorker, 'foo', 1, :bat => 'bar')
94
+ #
95
+ def enqueue_to_in(queue, interval, klass, *args)
96
+ int = interval.to_f
97
+ now = Time.now.to_f
98
+ ts = (int < 1_000_000_000 ? now + int : int)
99
+
100
+ item = { 'class' => klass, 'args' => args, 'at' => ts, 'queue' => queue }
101
+ item.delete('at') if ts <= now
102
+
103
+ klass.client_push(item)
104
+ end
105
+
106
+ # Example usage:
107
+ # Sidekiq::Client.enqueue_in(3.minutes, MyWorker, 'foo', 1, :bat => 'bar')
108
+ #
109
+ def enqueue_in(interval, klass, *args)
110
+ klass.perform_in(interval, *args)
111
+ end
112
+
91
113
  private
92
114
 
93
115
  def raw_push(payloads)
@@ -129,7 +151,7 @@ module Sidekiq
129
151
  normalized_item = item['class'].get_sidekiq_options.merge(item)
130
152
  normalized_item['class'] = normalized_item['class'].to_s
131
153
  else
132
- normalized_item = Sidekiq::Worker::ClassMethods::DEFAULT_OPTIONS.merge(item)
154
+ normalized_item = Sidekiq.default_worker_options.merge(item)
133
155
  end
134
156
 
135
157
  normalized_item['jid'] ||= SecureRandom.hex(12)
@@ -1,8 +1,8 @@
1
1
  module Sidekiq
2
2
  module ExceptionHandler
3
3
 
4
- def handle_exception(ex, ctxHash)
5
- Sidekiq.logger.warn ctxHash
4
+ def handle_exception(ex, ctxHash={})
5
+ Sidekiq.logger.warn(ctxHash) if !ctxHash.empty?
6
6
  Sidekiq.logger.warn ex
7
7
  Sidekiq.logger.warn ex.backtrace.join("\n")
8
8
  # This list of services is getting a bit ridiculous.
@@ -1,25 +1,54 @@
1
- require 'sidekiq/util'
1
+ require 'sidekiq/actor'
2
2
  require 'sidekiq/manager'
3
+ require 'sidekiq/fetch'
3
4
  require 'sidekiq/scheduled'
4
5
 
5
6
  module Sidekiq
7
+ # The Launcher is a very simple Actor whose job is to
8
+ # start, monitor and stop the core Actors in Sidekiq.
9
+ # If any of these actors die, the Sidekiq process exits
10
+ # immediately.
6
11
  class Launcher
7
- attr_reader :manager, :poller, :options
12
+ include Actor
13
+ include Util
14
+
15
+ trap_exit :actor_died
16
+
17
+ attr_reader :manager, :poller, :fetcher
18
+
8
19
  def initialize(options)
20
+ @manager = Sidekiq::Manager.new_link options
21
+ @poller = Sidekiq::Scheduled::Poller.new_link
22
+ @fetcher = Sidekiq::Fetcher.new_link @manager, options
23
+ @manager.fetcher = @fetcher
24
+ @done = false
9
25
  @options = options
10
- @manager = Sidekiq::Manager.new(options)
11
- @poller = Sidekiq::Scheduled::Poller.new
26
+ end
27
+
28
+ def actor_died(actor, reason)
29
+ return if @done
30
+ Sidekiq.logger.warn("Sidekiq died due to the following error, cannot recover, process exiting")
31
+ handle_exception(reason)
32
+ exit(1)
12
33
  end
13
34
 
14
35
  def run
15
- manager.async.start
16
- poller.async.poll(true)
36
+ watchdog('Launcher#run') do
37
+ manager.async.start
38
+ poller.async.poll(true)
39
+ end
17
40
  end
18
41
 
19
42
  def stop
20
- poller.async.terminate if poller.alive?
21
- manager.async.stop(:shutdown => true, :timeout => options[:timeout])
22
- manager.wait(:shutdown)
43
+ watchdog('Launcher#stop') do
44
+ @done = true
45
+ Sidekiq::Fetcher.done!
46
+ fetcher.async.terminate if fetcher.alive?
47
+ poller.async.terminate if poller.alive?
48
+
49
+ manager.async.stop(:shutdown => true, :timeout => @options[:timeout])
50
+ manager.wait(:shutdown)
51
+ end
23
52
  end
24
53
 
25
54
  def procline(tag)
@@ -28,7 +28,6 @@ module Sidekiq
28
28
  @threads = {}
29
29
  @done = false
30
30
  @busy = []
31
- @fetcher = Fetcher.new(current_actor, options)
32
31
  @ready = @count.times.map do
33
32
  p = Processor.new_link(current_actor)
34
33
  p.proxy_id = p.object_id
@@ -42,8 +41,6 @@ module Sidekiq
42
41
  timeout = options[:timeout]
43
42
 
44
43
  @done = true
45
- Sidekiq::Fetcher.done!
46
- @fetcher.async.terminate if @fetcher.alive?
47
44
 
48
45
  logger.info { "Shutting down #{@ready.size} quiet workers" }
49
46
  @ready.each { |x| x.terminate if x.alive? }
@@ -1,16 +1,77 @@
1
1
  module Sidekiq
2
2
 
3
+ class Testing
4
+ class << self
5
+ attr_accessor :__test_mode
6
+
7
+ def __set_test_mode(mode, &block)
8
+ if block
9
+ current_mode = self.__test_mode
10
+ begin
11
+ self.__test_mode = mode
12
+ block.call
13
+ ensure
14
+ self.__test_mode = current_mode
15
+ end
16
+ else
17
+ self.__test_mode = mode
18
+ end
19
+ end
20
+
21
+ def disable!(&block)
22
+ __set_test_mode(:disable, &block)
23
+ end
24
+
25
+ def fake!(&block)
26
+ __set_test_mode(:fake, &block)
27
+ end
28
+
29
+ def inline!(&block)
30
+ __set_test_mode(:inline, &block)
31
+ end
32
+
33
+ def enabled?
34
+ self.__test_mode != :disable
35
+ end
36
+
37
+ def disabled?
38
+ self.__test_mode == :disable
39
+ end
40
+
41
+ def fake?
42
+ self.__test_mode == :fake
43
+ end
44
+
45
+ def inline?
46
+ self.__test_mode == :inline
47
+ end
48
+ end
49
+ end
50
+
51
+ # Default to fake testing to keep old behavior
52
+ Sidekiq::Testing.fake!
53
+
3
54
  class EmptyQueueError < RuntimeError; end
4
55
 
5
56
  class Client
6
57
  class << self
7
- alias_method :raw_push_old, :raw_push
58
+ alias_method :raw_push_real, :raw_push
8
59
 
9
60
  def raw_push(payloads)
10
- payloads.each do |job|
11
- job['class'].constantize.jobs << Sidekiq.load_json(Sidekiq.dump_json(job))
61
+ if Sidekiq::Testing.fake?
62
+ payloads.each do |job|
63
+ job['class'].constantize.jobs << Sidekiq.load_json(Sidekiq.dump_json(job))
64
+ end
65
+ true
66
+ elsif Sidekiq::Testing.inline?
67
+ payloads.each do |item|
68
+ marshalled = Sidekiq.load_json(Sidekiq.dump_json(item))
69
+ marshalled['class'].constantize.new.perform(*marshalled['args'])
70
+ end
71
+ true
72
+ else
73
+ raw_push_real(payloads)
12
74
  end
13
- true
14
75
  end
15
76
  end
16
77
  end
@@ -1,41 +1,28 @@
1
- module Sidekiq
2
- class Client
1
+ require 'sidekiq/testing'
3
2
 
4
- ##
5
- # The Sidekiq inline infrastructure overrides perform_async so that it
6
- # actually calls perform instead. This allows workers to be run inline in a
7
- # testing environment.
8
- #
9
- # This is similar to `Resque.inline = true` functionality.
10
- #
11
- # Example:
12
- #
13
- # require 'sidekiq/testing/inline'
14
- #
15
- # $external_variable = 0
16
- #
17
- # class ExternalWorker
18
- # include Sidekiq::Worker
19
- #
20
- # def perform
21
- # $external_variable = 1
22
- # end
23
- # end
24
- #
25
- # assert_equal 0, $external_variable
26
- # ExternalWorker.perform_async
27
- # assert_equal 1, $external_variable
28
- #
29
- singleton_class.class_eval do
30
- alias_method :raw_push_old, :raw_push
31
- def raw_push(payload)
32
- [payload].flatten.each do |item|
33
- marshalled = Sidekiq.load_json(Sidekiq.dump_json(item))
34
- marshalled['class'].constantize.new.perform(*marshalled['args'])
35
- end
36
-
37
- true
38
- end
39
- end
40
- end
41
- end
3
+ ##
4
+ # The Sidekiq inline infrastructure overrides perform_async so that it
5
+ # actually calls perform instead. This allows workers to be run inline in a
6
+ # testing environment.
7
+ #
8
+ # This is similar to `Resque.inline = true` functionality.
9
+ #
10
+ # Example:
11
+ #
12
+ # require 'sidekiq/testing/inline'
13
+ #
14
+ # $external_variable = 0
15
+ #
16
+ # class ExternalWorker
17
+ # include Sidekiq::Worker
18
+ #
19
+ # def perform
20
+ # $external_variable = 1
21
+ # end
22
+ # end
23
+ #
24
+ # assert_equal 0, $external_variable
25
+ # ExternalWorker.perform_async
26
+ # assert_equal 1, $external_variable
27
+ #
28
+ Sidekiq::Testing.inline!
@@ -15,6 +15,7 @@ module Sidekiq
15
15
  yield
16
16
  rescue Exception => ex
17
17
  handle_exception(ex, { :context => last_words })
18
+ raise ex
18
19
  end
19
20
 
20
21
  def logger
@@ -1,3 +1,3 @@
1
1
  module Sidekiq
2
- VERSION = "2.14.1"
2
+ VERSION = "2.15.0"
3
3
  end
@@ -53,12 +53,21 @@ module Sidekiq
53
53
 
54
54
  def workers
55
55
  @workers ||= begin
56
- Sidekiq.redis do |conn|
56
+ to_rem = []
57
+ workers = Sidekiq.redis do |conn|
57
58
  conn.smembers('workers').map do |w|
58
59
  msg = conn.get("worker:#{w}")
59
- msg ? [w, Sidekiq.load_json(msg)] : nil
60
+ msg ? [w, Sidekiq.load_json(msg)] : (to_rem << w; nil)
60
61
  end.compact.sort { |x| x[1] ? -1 : 1 }
61
62
  end
63
+
64
+ # Detect and clear out any orphaned worker records.
65
+ # These can be left in Redis if Sidekiq crashes hard
66
+ # while processing jobs.
67
+ if to_rem.size > 0
68
+ Sidekiq.redis { |conn| conn.srem('workers', to_rem) }
69
+ end
70
+ workers
62
71
  end
63
72
  end
64
73
 
@@ -107,10 +116,14 @@ module Sidekiq
107
116
  [score.to_f, jid]
108
117
  end
109
118
 
119
+ def truncate(text, truncate_after_chars = 2000)
120
+ truncate_after_chars && text.size > truncate_after_chars ? "#{text[0..truncate_after_chars]}..." : text
121
+ end
122
+
110
123
  def display_args(args, truncate_after_chars = 2000)
111
124
  args.map do |arg|
112
125
  a = arg.inspect
113
- truncate_after_chars && a.size > truncate_after_chars ? "#{a[0..truncate_after_chars]}..." : a
126
+ truncate(a)
114
127
  end.join(", ")
115
128
  end
116
129
 
@@ -45,12 +45,12 @@ module Sidekiq
45
45
  now = Time.now.to_f
46
46
  ts = (int < 1_000_000_000 ? now + int : int)
47
47
 
48
+ item = { 'class' => self, 'args' => args, 'at' => ts }
49
+
48
50
  # Optimization to enqueue something now that is scheduled to go out now or in the past
49
- if ts <= now
50
- perform_async(*args)
51
- else
52
- client_push('class' => self, 'args' => args, 'at' => ts)
53
- end
51
+ item.delete('at') if ts <= now
52
+
53
+ client_push(item)
54
54
  end
55
55
  alias_method :perform_at, :perform_in
56
56
 
@@ -75,10 +75,8 @@ module Sidekiq
75
75
  self.sidekiq_retries_exhausted_block = block
76
76
  end
77
77
 
78
- DEFAULT_OPTIONS = { 'retry' => true, 'queue' => 'default' }
79
-
80
78
  def get_sidekiq_options # :nodoc:
81
- self.sidekiq_options_hash ||= DEFAULT_OPTIONS
79
+ self.sidekiq_options_hash ||= Sidekiq.default_worker_options
82
80
  end
83
81
 
84
82
  def client_push(item) # :nodoc: