sidekiq 2.8.0 → 2.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.

data/Changes.md CHANGED
@@ -1,3 +1,17 @@
1
+ 2.9.0
2
+ -----------
3
+
4
+ - Update 'sidekiq/testing' to work with any Sidekiq::Client call. It
5
+ also serializes the arguments as using Redis would. [#713]
6
+ - Raise a Sidekiq::Shutdown error within workers which don't finish within the hard
7
+ timeout. This is to prevent unwanted database transaction commits. [#377]
8
+ - Lazy load Redis connection pool, you no longer need to specify
9
+ anything in Passenger or Unicorn's after_fork callback [#794]
10
+ - Add optional Worker#retries_exhausted hook after max retries failed. [jkassemi, #780]
11
+ - Fix bug in pagination link to last page [pitr, #774]
12
+ - Upstart scripts for multiple Sidekiq instances [dariocravero, #763]
13
+ - Use select via pipes instead of poll to catch signals [mrnugget, #761]
14
+
1
15
  2.8.0
2
16
  -----------
3
17
 
data/README.md CHANGED
@@ -1,8 +1,11 @@
1
1
  Sidekiq
2
2
  ==============
3
3
 
4
- [![Build Status](https://secure.travis-ci.org/mperham/sidekiq.png)](http://travis-ci.org/mperham/sidekiq)
5
- [![Dependency Status](https://gemnasium.com/mperham/sidekiq.png)](https://gemnasium.com/mperham/sidekiq)
4
+ - [![Gem Version](https://badge.fury.io/rb/sidekiq.png)](https://rubygems.org/gems/sidekiq)
5
+ - [![Code Climate](https://codeclimate.com/github/mperham/sidekiq.png)](https://codeclimate.com/github/mperham/sidekiq)
6
+ - [![Build Status](https://travis-ci.org/mperham/sidekiq.png)](https://travis-ci.org/mperham/sidekiq)
7
+ - [![Coverage Status](https://coveralls.io/repos/mperham/sidekiq/badge.png?branch=master)](https://coveralls.io/r/mperham/sidekiq)
8
+
6
9
 
7
10
  Simple, efficient message processing for Ruby.
8
11
 
@@ -43,7 +46,10 @@ Getting Started
43
46
  -----------------
44
47
 
45
48
  See the [sidekiq home page](http://mperham.github.com/sidekiq) for the simple 4-step process.
46
- You can watch [Railscast #366](http://railscasts.com/episodes/366-sidekiq) to see Sidekiq in action.
49
+ You can watch [Railscast #366](http://railscasts.com/episodes/366-sidekiq) to see Sidekiq in action. If you do everything right, you should see this:
50
+
51
+ ![Web UI](https://github.com/mperham/sidekiq/raw/master/examples/web-ui.png)
52
+
47
53
 
48
54
 
49
55
  More Information
@@ -27,7 +27,7 @@ module Sidekiq
27
27
  :profile => false,
28
28
  }
29
29
 
30
- def self.❨╯°□°❩╯︵ ┻━┻
30
+ def self.❨╯°□°❩╯︵┻━┻
31
31
  puts "Calm down, bro"
32
32
  end
33
33
 
@@ -68,16 +68,15 @@ module Sidekiq
68
68
 
69
69
  def self.redis(&block)
70
70
  raise ArgumentError, "requires a block" if !block
71
- @redis ||= Sidekiq::RedisConnection.create
71
+ @redis ||= Sidekiq::RedisConnection.create(@hash || {})
72
72
  @redis.with(&block)
73
73
  end
74
74
 
75
75
  def self.redis=(hash)
76
+ return @redis = hash if hash.is_a?(ConnectionPool)
77
+
76
78
  if hash.is_a?(Hash)
77
- @redis = RedisConnection.create(hash)
78
- options[:namespace] ||= hash[:namespace]
79
- elsif hash.is_a?(ConnectionPool)
80
- @redis = hash
79
+ @hash = hash
81
80
  else
82
81
  raise ArgumentError, "redis= requires a Hash or ConnectionPool"
83
82
  end
@@ -115,16 +114,4 @@ module Sidekiq
115
114
  self.options[:poll_interval] = interval
116
115
  end
117
116
 
118
- ##
119
- # deprecated
120
- def self.size(*queues)
121
- return Sidekiq::Stats.new.enqueued if queues.empty?
122
-
123
- Sidekiq.redis { |conn|
124
- conn.multi {
125
- queues.map { |q| conn.llen("queue:#{q}") }
126
- }
127
- }.inject(0) { |memo, count| memo += count }
128
- end
129
-
130
117
  end
@@ -1,14 +1,3 @@
1
- $sidekiq_signals = []
2
-
3
- # Signal handlers should do as little as humanly possible
4
- # and defer all work to a non-trap context. We'll have
5
- # the main thread poll for signals and handle them there.
6
- %w(INT TERM USR1 USR2 TTIN).each do |sig|
7
- trap sig do
8
- $sidekiq_signals << sig
9
- end
10
- end
11
-
12
1
  $stdout.sync = true
13
2
 
14
3
  require 'yaml'
@@ -20,6 +9,11 @@ require 'sidekiq'
20
9
  require 'sidekiq/util'
21
10
 
22
11
  module Sidekiq
12
+ # Used to raise in workers that have not finished within the
13
+ # hard timeout limit. This is needed to rollback db transactions,
14
+ # otherwise Ruby's Thread#kill will commit. See #377.
15
+ class Shutdown < RuntimeError; end
16
+
23
17
  class CLI
24
18
  include Util
25
19
  include Singleton
@@ -31,8 +25,6 @@ module Sidekiq
31
25
 
32
26
  def initialize
33
27
  @code = nil
34
- @interrupt_mutex = Mutex.new
35
- @interrupted = false
36
28
  end
37
29
 
38
30
  def parse(args=ARGV)
@@ -48,6 +40,14 @@ module Sidekiq
48
40
  end
49
41
 
50
42
  def run
43
+ self_read, self_write = IO.pipe
44
+
45
+ %w(INT TERM USR1 USR2 TTIN).each do |sig|
46
+ trap sig do
47
+ self_write.puts(sig)
48
+ end
49
+ end
50
+
51
51
  logger.info "Booting Sidekiq #{Sidekiq::VERSION} with Redis at #{redis {|x| x.client.id}}"
52
52
  logger.info "Running in #{RUBY_DESCRIPTION}"
53
53
  logger.info Sidekiq::LICENSE
@@ -69,9 +69,9 @@ module Sidekiq
69
69
  end
70
70
  launcher.run
71
71
 
72
- while true
73
- handle_signals
74
- sleep 1
72
+ while readable_io = IO.select([self_read])
73
+ signal = readable_io.first[0].gets.strip
74
+ handle_signal(signal)
75
75
  end
76
76
  rescue Interrupt
77
77
  logger.info 'Shutting down'
@@ -82,47 +82,45 @@ module Sidekiq
82
82
  end
83
83
  end
84
84
 
85
- def handle_signals
86
- while sig = $sidekiq_signals.shift
87
- Sidekiq.logger.debug "Got #{sig} signal"
88
- case sig
89
- when 'INT'
90
- if Sidekiq.options[:profile]
91
- result = RubyProf.stop
92
- printer = RubyProf::GraphHtmlPrinter.new(result)
93
- File.open("profile.html", 'w') do |f|
94
- printer.print(f, :min_percent => 1)
95
- end
96
- end
97
- # Handle Ctrl-C in JRuby like MRI
98
- # http://jira.codehaus.org/browse/JRUBY-4637
99
- raise Interrupt
100
- when 'TERM'
101
- # Heroku sends TERM and then waits 10 seconds for process to exit.
102
- raise Interrupt
103
- when 'USR1'
104
- Sidekiq.logger.info "Received USR1, no longer accepting new work"
105
- launcher.manager.async.stop
106
- when 'USR2'
107
- if Sidekiq.options[:logfile]
108
- Sidekiq.logger.info "Received USR2, reopening log file"
109
- Sidekiq::Logging.initialize_logger(Sidekiq.options[:logfile])
85
+ private
86
+
87
+ def handle_signal(sig)
88
+ Sidekiq.logger.debug "Got #{sig} signal"
89
+ case sig
90
+ when 'INT'
91
+ if Sidekiq.options[:profile]
92
+ result = RubyProf.stop
93
+ printer = RubyProf::GraphHtmlPrinter.new(result)
94
+ File.open("profile.html", 'w') do |f|
95
+ printer.print(f, :min_percent => 1)
110
96
  end
111
- when 'TTIN'
112
- Thread.list.each do |thread|
113
- Sidekiq.logger.info "Thread TID-#{thread.object_id.to_s(36)} #{thread['label']}"
114
- if thread.backtrace
115
- Sidekiq.logger.info thread.backtrace.join("\n")
116
- else
117
- Sidekiq.logger.info "<no backtrace available>"
118
- end
97
+ end
98
+ # Handle Ctrl-C in JRuby like MRI
99
+ # http://jira.codehaus.org/browse/JRUBY-4637
100
+ raise Interrupt
101
+ when 'TERM'
102
+ # Heroku sends TERM and then waits 10 seconds for process to exit.
103
+ raise Interrupt
104
+ when 'USR1'
105
+ Sidekiq.logger.info "Received USR1, no longer accepting new work"
106
+ launcher.manager.async.stop
107
+ when 'USR2'
108
+ if Sidekiq.options[:logfile]
109
+ Sidekiq.logger.info "Received USR2, reopening log file"
110
+ Sidekiq::Logging.initialize_logger(Sidekiq.options[:logfile])
111
+ end
112
+ when 'TTIN'
113
+ Thread.list.each do |thread|
114
+ Sidekiq.logger.info "Thread TID-#{thread.object_id.to_s(36)} #{thread['label']}"
115
+ if thread.backtrace
116
+ Sidekiq.logger.info thread.backtrace.join("\n")
117
+ else
118
+ Sidekiq.logger.info "<no backtrace available>"
119
119
  end
120
120
  end
121
121
  end
122
122
  end
123
123
 
124
- private
125
-
126
124
  def load_celluloid
127
125
  raise "Celluloid cannot be required until here, or it will break Sidekiq's daemonization" if defined?(::Celluloid) && options[:daemon]
128
126
 
@@ -4,134 +4,135 @@ require 'sidekiq/middleware/chain'
4
4
 
5
5
  module Sidekiq
6
6
  class Client
7
+ class << self
7
8
 
8
- def self.default_middleware
9
- Middleware::Chain.new do |m|
9
+ def default_middleware
10
+ Middleware::Chain.new do
11
+ end
10
12
  end
11
- end
12
-
13
- def self.registered_workers
14
- Sidekiq.redis { |x| x.smembers('workers') }
15
- end
16
13
 
17
- def self.registered_queues
18
- Sidekiq.redis { |x| x.smembers('queues') }
19
- end
14
+ def registered_workers
15
+ Sidekiq.redis { |x| x.smembers('workers') }
16
+ end
20
17
 
21
- ##
22
- # The main method used to push a job to Redis. Accepts a number of options:
23
- #
24
- # queue - the named queue to use, default 'default'
25
- # class - the worker class to call, required
26
- # args - an array of simple arguments to the perform method, must be JSON-serializable
27
- # retry - whether to retry this job if it fails, true or false, default true
28
- # backtrace - whether to save any error backtrace, default false
29
- #
30
- # All options must be strings, not symbols. NB: because we are serializing to JSON, all
31
- # symbols in 'args' will be converted to strings.
32
- #
33
- # Returns nil if not pushed to Redis or a unique Job ID if pushed.
34
- #
35
- # Example:
36
- # Sidekiq::Client.push('queue' => 'my_queue', 'class' => MyWorker, 'args' => ['foo', 1, :bat => 'bar'])
37
- #
38
- def self.push(item)
39
- normed = normalize_item(item)
40
- normed, payload = process_single(item['class'], normed)
41
-
42
- pushed = false
43
- pushed = raw_push(normed, payload) if normed
44
- pushed ? normed['jid'] : nil
45
- end
18
+ def registered_queues
19
+ Sidekiq.redis { |x| x.smembers('queues') }
20
+ end
46
21
 
47
- ##
48
- # Push a large number of jobs to Redis. In practice this method is only
49
- # useful if you are pushing tens of thousands of jobs or more. This method
50
- # basically cuts down on the redis round trip latency.
51
- #
52
- # Takes the same arguments as Client.push except that args is expected to be
53
- # an Array of Arrays. All other keys are duplicated for each job. Each job
54
- # is run through the client middleware pipeline and each job gets its own Job ID
55
- # as normal.
56
- #
57
- # Returns the number of jobs pushed or nil if the pushed failed. The number of jobs
58
- # pushed can be less than the number given if the middleware stopped processing for one
59
- # or more jobs.
60
- def self.push_bulk(items)
61
- normed = normalize_item(items)
62
- payloads = items['args'].map do |args|
63
- _, payload = process_single(items['class'], normed.merge('args' => args, 'jid' => SecureRandom.hex(12)))
64
- payload
65
- end.compact
66
-
67
- pushed = false
68
- pushed = raw_push(normed, payloads)
69
- pushed ? payloads.size : nil
70
- end
22
+ ##
23
+ # The main method used to push a job to Redis. Accepts a number of options:
24
+ #
25
+ # queue - the named queue to use, default 'default'
26
+ # class - the worker class to call, required
27
+ # args - an array of simple arguments to the perform method, must be JSON-serializable
28
+ # retry - whether to retry this job if it fails, true or false, default true
29
+ # backtrace - whether to save any error backtrace, default false
30
+ #
31
+ # All options must be strings, not symbols. NB: because we are serializing to JSON, all
32
+ # symbols in 'args' will be converted to strings.
33
+ #
34
+ # Returns nil if not pushed to Redis or a unique Job ID if pushed.
35
+ #
36
+ # Example:
37
+ # Sidekiq::Client.push('queue' => 'my_queue', 'class' => MyWorker, 'args' => ['foo', 1, :bat => 'bar'])
38
+ #
39
+ def push(item)
40
+ normed = normalize_item(item)
41
+ payload = process_single(item['class'], normed)
42
+
43
+ pushed = false
44
+ pushed = raw_push([payload]) if payload
45
+ pushed ? payload['jid'] : nil
46
+ end
71
47
 
72
- # Resque compatibility helpers.
73
- #
74
- # Example usage:
75
- # Sidekiq::Client.enqueue(MyWorker, 'foo', 1, :bat => 'bar')
76
- #
77
- # Messages are enqueued to the 'default' queue.
78
- #
79
- def self.enqueue(klass, *args)
80
- klass.client_push('class' => klass, 'args' => args)
81
- end
48
+ ##
49
+ # Push a large number of jobs to Redis. In practice this method is only
50
+ # useful if you are pushing tens of thousands of jobs or more. This method
51
+ # basically cuts down on the redis round trip latency.
52
+ #
53
+ # Takes the same arguments as Client.push except that args is expected to be
54
+ # an Array of Arrays. All other keys are duplicated for each job. Each job
55
+ # is run through the client middleware pipeline and each job gets its own Job ID
56
+ # as normal.
57
+ #
58
+ # Returns the number of jobs pushed or nil if the pushed failed. The number of jobs
59
+ # pushed can be less than the number given if the middleware stopped processing for one
60
+ # or more jobs.
61
+ def push_bulk(items)
62
+ normed = normalize_item(items)
63
+ payloads = items['args'].map do |args|
64
+ raise ArgumentError, "Bulk arguments must be an Array of Arrays: [[1], [2]]" if !args.is_a?(Array)
65
+ process_single(items['class'], normed.merge('args' => args, 'jid' => SecureRandom.hex(12)))
66
+ end.compact
67
+
68
+ pushed = false
69
+ pushed = raw_push(payloads) if !payloads.empty?
70
+ pushed ? payloads.size : nil
71
+ end
82
72
 
83
- # Example usage:
84
- # Sidekiq::Client.enqueue_to(:queue_name, MyWorker, 'foo', 1, :bat => 'bar')
85
- #
86
- def self.enqueue_to(queue, klass, *args)
87
- klass.client_push('queue' => queue, 'class' => klass, 'args' => args)
88
- end
73
+ # Resque compatibility helpers.
74
+ #
75
+ # Example usage:
76
+ # Sidekiq::Client.enqueue(MyWorker, 'foo', 1, :bat => 'bar')
77
+ #
78
+ # Messages are enqueued to the 'default' queue.
79
+ #
80
+ def enqueue(klass, *args)
81
+ klass.client_push('class' => klass, 'args' => args)
82
+ end
89
83
 
90
- private
84
+ # Example usage:
85
+ # Sidekiq::Client.enqueue_to(:queue_name, MyWorker, 'foo', 1, :bat => 'bar')
86
+ #
87
+ def enqueue_to(queue, klass, *args)
88
+ klass.client_push('queue' => queue, 'class' => klass, 'args' => args)
89
+ end
91
90
 
92
- def self.raw_push(normed, payload) # :nodoc:
93
- pushed = false
94
- Sidekiq.redis do |conn|
95
- if normed['at'] && payload.is_a?(Array)
96
- pushed = conn.zadd('schedule', payload.map {|hash| [normed['at'].to_s, hash]})
97
- elsif normed['at']
98
- pushed = conn.zadd('schedule', normed['at'].to_s, payload)
99
- else
100
- _, pushed = conn.multi do
101
- conn.sadd('queues', normed['queue'])
102
- conn.lpush("queue:#{normed['queue']}", payload)
91
+ private
92
+
93
+ def raw_push(payloads)
94
+ pushed = false
95
+ Sidekiq.redis do |conn|
96
+ if payloads.first['at']
97
+ pushed = conn.zadd('schedule', payloads.map {|hash| [hash['at'].to_s, Sidekiq.dump_json(hash)]})
98
+ else
99
+ q = payloads.first['queue']
100
+ to_push = payloads.map { |entry| Sidekiq.dump_json(entry) }
101
+ _, pushed = conn.multi do
102
+ conn.sadd('queues', q)
103
+ conn.lpush("queue:#{q}", to_push)
104
+ end
103
105
  end
104
106
  end
107
+ pushed
105
108
  end
106
- pushed
107
- end
108
109
 
109
- def self.process_single(worker_class, item)
110
- queue = item['queue']
110
+ def process_single(worker_class, item)
111
+ queue = item['queue']
111
112
 
112
- Sidekiq.client_middleware.invoke(worker_class, item, queue) do
113
- payload = Sidekiq.dump_json(item)
114
- return item, payload
113
+ Sidekiq.client_middleware.invoke(worker_class, item, queue) do
114
+ item
115
+ end
115
116
  end
116
- end
117
117
 
118
- def self.normalize_item(item)
119
- raise(ArgumentError, "Message must be a Hash of the form: { 'class' => SomeWorker, 'args' => ['bob', 1, :foo => 'bar'] }") unless item.is_a?(Hash)
120
- raise(ArgumentError, "Message must include a class and set of arguments: #{item.inspect}") if !item['class'] || !item['args']
121
- raise(ArgumentError, "Message args must be an Array") unless item['args'].is_a?(Array)
122
- raise(ArgumentError, "Message class must be either a Class or String representation of the class name") unless item['class'].is_a?(Class) || item['class'].is_a?(String)
123
-
124
- if item['class'].is_a?(Class)
125
- raise(ArgumentError, "Message must include a Sidekiq::Worker class, not class name: #{item['class'].ancestors.inspect}") if !item['class'].respond_to?('get_sidekiq_options')
126
- normalized_item = item['class'].get_sidekiq_options.merge(item)
127
- normalized_item['class'] = normalized_item['class'].to_s
128
- else
129
- normalized_item = Sidekiq::Worker::ClassMethods::DEFAULT_OPTIONS.merge(item)
118
+ def normalize_item(item)
119
+ raise(ArgumentError, "Message must be a Hash of the form: { 'class' => SomeWorker, 'args' => ['bob', 1, :foo => 'bar'] }") unless item.is_a?(Hash)
120
+ raise(ArgumentError, "Message must include a class and set of arguments: #{item.inspect}") if !item['class'] || !item['args']
121
+ raise(ArgumentError, "Message args must be an Array") unless item['args'].is_a?(Array)
122
+ raise(ArgumentError, "Message class must be either a Class or String representation of the class name") unless item['class'].is_a?(Class) || item['class'].is_a?(String)
123
+
124
+ if item['class'].is_a?(Class)
125
+ raise(ArgumentError, "Message must include a Sidekiq::Worker class, not class name: #{item['class'].ancestors.inspect}") if !item['class'].respond_to?('get_sidekiq_options')
126
+ normalized_item = item['class'].get_sidekiq_options.merge(item)
127
+ normalized_item['class'] = normalized_item['class'].to_s
128
+ else
129
+ normalized_item = Sidekiq::Worker::ClassMethods::DEFAULT_OPTIONS.merge(item)
130
+ end
131
+
132
+ normalized_item['jid'] = SecureRandom.hex(12)
133
+ normalized_item
130
134
  end
131
135
 
132
- normalized_item['jid'] = SecureRandom.hex(12)
133
- normalized_item
134
136
  end
135
-
136
137
  end
137
138
  end
@@ -110,7 +110,7 @@ module Sidekiq
110
110
 
111
111
  def hard_shutdown_in(delay)
112
112
  after(delay) do
113
- watchdog("Manager#watch_for_shutdown died") do
113
+ watchdog("Manager#hard_shutdown_in died") do
114
114
  # We've reached the timeout and we still have busy workers.
115
115
  # They must die but their messages shall live on.
116
116
  logger.info("Still waiting for #{@busy.size} busy workers")
@@ -138,7 +138,8 @@ module Sidekiq
138
138
 
139
139
  logger.debug { "Terminating worker threads" }
140
140
  @busy.each do |processor|
141
- processor.terminate if processor.alive?
141
+ t = processor.bare_object.actual_work_thread
142
+ t.raise Shutdown if processor.alive?
142
143
  end
143
144
 
144
145
  after(0) { signal(:shutdown) }
@@ -14,7 +14,9 @@ module Sidekiq
14
14
  # 3. after a few days, a developer deploys a fix. the message is
15
15
  # reprocessed successfully.
16
16
  # 4. if 3 never happens, sidekiq will eventually give up and throw the
17
- # message away.
17
+ # message away. If the worker defines a method called 'retries_exhausted',
18
+ # this will be called before throwing the message away. If the
19
+ # 'retries_exhausted' method throws an exception, it's dropped and logged.
18
20
  #
19
21
  # A message looks like:
20
22
  #
@@ -43,7 +45,6 @@ module Sidekiq
43
45
 
44
46
  # delayed_job uses the same basic formula
45
47
  DEFAULT_MAX_RETRY_ATTEMPTS = 25
46
- DELAY = proc { |count| (count ** 4) + 15 + (rand(30)*(count+1)) }
47
48
 
48
49
  def call(worker, msg, queue)
49
50
  yield
@@ -73,7 +74,7 @@ module Sidekiq
73
74
  end
74
75
 
75
76
  if count < max_retry_attempts
76
- delay = DELAY.call(count)
77
+ delay = seconds_to_delay(count)
77
78
  logger.debug { "Failure! Retry #{count} in #{delay} seconds" }
78
79
  retry_at = Time.now.to_f + delay
79
80
  payload = Sidekiq.dump_json(msg)
@@ -82,11 +83,20 @@ module Sidekiq
82
83
  end
83
84
  else
84
85
  # Goodbye dear message, you (re)tried your best I'm sure.
85
- logger.debug { "Dropping message after hitting the retry maximum: #{msg}" }
86
+ retries_exhausted(worker, msg)
86
87
  end
88
+
87
89
  raise e
88
90
  end
89
91
 
92
+ def retries_exhausted(worker, msg)
93
+ logger.debug { "Dropping message after hitting the retry maximum: #{msg}" }
94
+ worker.retries_exhausted(*msg['args']) if worker.respond_to?(:retries_exhausted)
95
+
96
+ rescue Exception => e
97
+ handle_exception(e, "Error calling retries_exhausted")
98
+ end
99
+
90
100
  def retry_attempts_from(msg_retry, default)
91
101
  if msg_retry.is_a?(Fixnum)
92
102
  msg_retry
@@ -95,6 +105,10 @@ module Sidekiq
95
105
  end
96
106
  end
97
107
 
108
+ def seconds_to_delay(count)
109
+ (count ** 4) + 15 + (rand(30)*(count+1))
110
+ end
111
+
98
112
  end
99
113
  end
100
114
  end
@@ -15,8 +15,6 @@ module Sidekiq
15
15
  include Util
16
16
  include Celluloid
17
17
 
18
- # exclusive :process
19
-
20
18
  def self.default_middleware
21
19
  Middleware::Chain.new do |m|
22
20
  m.add Middleware::Server::Logging
@@ -26,6 +24,11 @@ module Sidekiq
26
24
  end
27
25
  end
28
26
 
27
+ # store the actual working thread so we
28
+ # can later kill if it necessary during
29
+ # hard shutdown.
30
+ attr_accessor :actual_work_thread
31
+
29
32
  def initialize(boss)
30
33
  @boss = boss
31
34
  end
@@ -34,6 +37,7 @@ module Sidekiq
34
37
  msgstr = work.message
35
38
  queue = work.queue_name
36
39
  defer do
40
+ @actual_work_thread = Thread.current
37
41
  begin
38
42
  msg = Sidekiq.load_json(msgstr)
39
43
  klass = msg['class'].constantize
@@ -45,6 +49,9 @@ module Sidekiq
45
49
  worker.perform(*cloned(msg['args']))
46
50
  end
47
51
  end
52
+ rescue Sidekiq::Shutdown
53
+ # Had to force kill this job because it didn't finish
54
+ # within the timeout.
48
55
  rescue Exception => ex
49
56
  handle_exception(ex, msg || { :message => msgstr })
50
57
  raise
@@ -60,19 +67,19 @@ module Sidekiq
60
67
  "#<Processor #{to_s}>"
61
68
  end
62
69
 
63
- def to_s
70
+ private
71
+
72
+ def identity
64
73
  @str ||= "#{hostname}:#{process_id}-#{Thread.current.object_id}:default"
65
74
  end
66
75
 
67
- private
68
-
69
76
  def stats(worker, msg, queue)
70
77
  redis do |conn|
71
78
  conn.multi do
72
- conn.sadd('workers', self)
73
- conn.setex("worker:#{self}:started", EXPIRY, Time.now.to_s)
79
+ conn.sadd('workers', identity)
80
+ conn.setex("worker:#{identity}:started", EXPIRY, Time.now.to_s)
74
81
  hash = {:queue => queue, :payload => msg, :run_at => Time.now.to_i }
75
- conn.setex("worker:#{self}", EXPIRY, Sidekiq.dump_json(hash))
82
+ conn.setex("worker:#{identity}", EXPIRY, Sidekiq.dump_json(hash))
76
83
  end
77
84
  end
78
85
 
@@ -89,9 +96,9 @@ module Sidekiq
89
96
  ensure
90
97
  redis do |conn|
91
98
  conn.multi do
92
- conn.srem("workers", self)
93
- conn.del("worker:#{self}")
94
- conn.del("worker:#{self}:started")
99
+ conn.srem("workers", identity)
100
+ conn.del("worker:#{identity}")
101
+ conn.del("worker:#{identity}:started")
95
102
  conn.incrby("stat:processed", 1)
96
103
  conn.incrby("stat:processed:#{Time.now.utc.to_date}", 1)
97
104
  end
@@ -1,24 +1,22 @@
1
1
  require 'connection_pool'
2
2
  require 'redis'
3
- require 'redis/namespace'
4
3
 
5
4
  module Sidekiq
6
5
  class RedisConnection
7
6
  def self.create(options={})
8
7
  url = options[:url] || determine_redis_provider || 'redis://localhost:6379/0'
9
- driver = options[:driver] || 'ruby'
10
8
  # need a connection for Fetcher and Retry
11
9
  size = options[:size] || (Sidekiq.server? ? (Sidekiq.options[:concurrency] + 2) : 5)
12
- namespace = options[:namespace]
13
10
 
14
11
  ConnectionPool.new(:timeout => 1, :size => size) do
15
- build_client(url, namespace, driver)
12
+ build_client(url, options[:namespace], options[:driver] || 'ruby')
16
13
  end
17
14
  end
18
15
 
19
16
  def self.build_client(url, namespace, driver)
20
17
  client = Redis.connect(:url => url, :driver => driver)
21
18
  if namespace
19
+ require 'redis/namespace'
22
20
  Redis::Namespace.new(namespace, :redis => client)
23
21
  else
24
22
  client
@@ -28,6 +26,7 @@ module Sidekiq
28
26
 
29
27
  # Not public
30
28
  def self.determine_redis_provider
29
+ # REDISTOGO_URL is only support for legacy reasons
31
30
  return ENV['REDISTOGO_URL'] if ENV['REDISTOGO_URL']
32
31
  provider = ENV['REDIS_PROVIDER'] || 'REDIS_URL'
33
32
  ENV[provider]
@@ -1,4 +1,18 @@
1
1
  module Sidekiq
2
+
3
+ class Client
4
+ class << self
5
+ alias_method :raw_push_old, :raw_push
6
+
7
+ def raw_push(payloads)
8
+ payloads.each do |job|
9
+ job['class'].constantize.jobs << Sidekiq.load_json(Sidekiq.dump_json(job))
10
+ end
11
+ true
12
+ end
13
+ end
14
+ end
15
+
2
16
  module Worker
3
17
  ##
4
18
  # The Sidekiq testing infrastructure overrides perform_async
@@ -56,12 +70,6 @@ module Sidekiq
56
70
  # Then I should receive a welcome email to "foo@example.com"
57
71
  #
58
72
  module ClassMethods
59
- alias_method :client_push_old, :client_push
60
-
61
- def client_push(opts)
62
- jobs << opts
63
- opts.object_id
64
- end
65
73
 
66
74
  # Jobs queued for this worker
67
75
  def jobs
@@ -28,9 +28,10 @@ module Sidekiq
28
28
  #
29
29
  singleton_class.class_eval do
30
30
  alias_method :raw_push_old, :raw_push
31
- def raw_push(normed, payload)
32
- Array(payload).each do |hash|
33
- normed['class'].constantize.new.perform(*Sidekiq.load_json(hash)['args'])
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'])
34
35
  end
35
36
 
36
37
  true
@@ -1,3 +1,3 @@
1
1
  module Sidekiq
2
- VERSION = "2.8.0"
2
+ VERSION = "2.9.0"
3
3
  end
@@ -50,7 +50,7 @@ module Sidekiq
50
50
  end
51
51
 
52
52
  def namespace
53
- Sidekiq.options[:namespace]
53
+ @@ns ||= Sidekiq.redis {|conn| conn.respond_to?(:namespace) ? conn.namespace : nil }
54
54
  end
55
55
 
56
56
  def root_path
@@ -25,4 +25,5 @@ Gem::Specification.new do |gem|
25
25
  gem.add_development_dependency 'rake'
26
26
  gem.add_development_dependency 'actionmailer', '~> 3'
27
27
  gem.add_development_dependency 'activerecord', '~> 3'
28
+ gem.add_development_dependency 'coveralls'
28
29
  end
@@ -1,3 +1,8 @@
1
+ require 'coveralls'
2
+ Coveralls.wear! do
3
+ add_filter "/test/"
4
+ end
5
+
1
6
  ENV['RACK_ENV'] = ENV['RAILS_ENV'] = 'test'
2
7
  if ENV.has_key?("SIMPLECOV")
3
8
  require 'simplecov'
@@ -272,7 +272,7 @@ class TestApi < MiniTest::Unit::TestCase
272
272
  it 'can enumerate workers' do
273
273
  w = Sidekiq::Workers.new
274
274
  assert_equal 0, w.size
275
- w.each do |x, y|
275
+ w.each do
276
276
  assert false
277
277
  end
278
278
 
@@ -46,7 +46,7 @@ class TestClient < MiniTest::Unit::TestCase
46
46
  end
47
47
 
48
48
  it 'pushes messages to redis' do
49
- @redis.expect :lpush, 1, ['queue:foo', String]
49
+ @redis.expect :lpush, 1, ['queue:foo', Array]
50
50
  pushed = Sidekiq::Client.push('queue' => 'foo', 'class' => MyWorker, 'args' => [1, 2])
51
51
  assert pushed
52
52
  assert_equal 24, pushed.size
@@ -54,7 +54,7 @@ class TestClient < MiniTest::Unit::TestCase
54
54
  end
55
55
 
56
56
  it 'pushes messages to redis using a String class' do
57
- @redis.expect :lpush, 1, ['queue:foo', String]
57
+ @redis.expect :lpush, 1, ['queue:foo', Array]
58
58
  pushed = Sidekiq::Client.push('queue' => 'foo', 'class' => 'MyWorker', 'args' => [1, 2])
59
59
  assert pushed
60
60
  assert_equal 24, pushed.size
@@ -70,28 +70,28 @@ class TestClient < MiniTest::Unit::TestCase
70
70
  end
71
71
 
72
72
  it 'handles perform_async' do
73
- @redis.expect :lpush, 1, ['queue:default', String]
73
+ @redis.expect :lpush, 1, ['queue:default', Array]
74
74
  pushed = MyWorker.perform_async(1, 2)
75
75
  assert pushed
76
76
  @redis.verify
77
77
  end
78
78
 
79
79
  it 'handles perform_async on failure' do
80
- @redis.expect :lpush, nil, ['queue:default', String]
80
+ @redis.expect :lpush, nil, ['queue:default', Array]
81
81
  pushed = MyWorker.perform_async(1, 2)
82
82
  refute pushed
83
83
  @redis.verify
84
84
  end
85
85
 
86
86
  it 'enqueues messages to redis' do
87
- @redis.expect :lpush, 1, ['queue:default', String]
87
+ @redis.expect :lpush, 1, ['queue:default', Array]
88
88
  pushed = Sidekiq::Client.enqueue(MyWorker, 1, 2)
89
89
  assert pushed
90
90
  @redis.verify
91
91
  end
92
92
 
93
93
  it 'enqueues messages to redis' do
94
- @redis.expect :lpush, 1, ['queue:custom_queue', String]
94
+ @redis.expect :lpush, 1, ['queue:custom_queue', Array]
95
95
  pushed = Sidekiq::Client.enqueue_to(:custom_queue, MyWorker, 1, 2)
96
96
  assert pushed
97
97
  @redis.verify
@@ -103,7 +103,7 @@ class TestClient < MiniTest::Unit::TestCase
103
103
  end
104
104
 
105
105
  it 'enqueues to the named queue' do
106
- @redis.expect :lpush, 1, ['queue:flimflam', String]
106
+ @redis.expect :lpush, 1, ['queue:flimflam', Array]
107
107
  pushed = QueuedWorker.perform_async(1, 2)
108
108
  assert pushed
109
109
  @redis.verify
@@ -125,12 +125,10 @@ class TestClient < MiniTest::Unit::TestCase
125
125
  Sidekiq::Queue.new.clear
126
126
  end
127
127
  it 'can push a large set of jobs at once' do
128
- a = Time.now
129
128
  count = Sidekiq::Client.push_bulk('class' => QueuedWorker, 'args' => (1..1_000).to_a.map { |x| Array(x) })
130
129
  assert_equal 1_000, count
131
130
  end
132
131
  it 'can push a large set of jobs at once using a String class' do
133
- a = Time.now
134
132
  count = Sidekiq::Client.push_bulk('class' => 'QueuedWorker', 'args' => (1..1_000).to_a.map { |x| Array(x) })
135
133
  assert_equal 1_000, count
136
134
  end
@@ -178,11 +176,11 @@ class TestClient < MiniTest::Unit::TestCase
178
176
 
179
177
  describe 'item normalization' do
180
178
  it 'defaults retry to true' do
181
- assert_equal true, Sidekiq::Client.normalize_item('class' => QueuedWorker, 'args' => [])['retry']
179
+ assert_equal true, Sidekiq::Client.send(:normalize_item, 'class' => QueuedWorker, 'args' => [])['retry']
182
180
  end
183
181
 
184
182
  it "does not normalize numeric retry's" do
185
- assert_equal 2, Sidekiq::Client.normalize_item('class' => CWorker, 'args' => [])['retry']
183
+ assert_equal 2, Sidekiq::Client.send(:normalize_item, 'class' => CWorker, 'args' => [])['retry']
186
184
  end
187
185
  end
188
186
  end
@@ -167,6 +167,36 @@ class TestRetry < MiniTest::Unit::TestCase
167
167
  # MiniTest can't assert that a method call did NOT happen!?
168
168
  assert_raises(MockExpectationError) { @redis.verify }
169
169
  end
170
+
171
+ describe "retry exhaustion" do
172
+ let(:worker){ MiniTest::Mock.new }
173
+ let(:handler){ Sidekiq::Middleware::Server::RetryJobs.new }
174
+ let(:msg){ {"class"=>"Bob", "args"=>[1, 2, "foo"], "queue"=>"default", "error_message"=>"kerblammo!", "error_class"=>"RuntimeError", "failed_at"=>Time.now.utc, "retry"=>3, "retry_count"=>3} }
175
+
176
+ it 'calls worker retries_exhausted after too many retries' do
177
+ worker.expect(:retries_exhausted, true, [1,2,"foo"])
178
+ task_misbehaving_worker
179
+ worker.verify
180
+ end
181
+
182
+ it 'handles and logs retries_exhausted failures gracefully (drops them)' do
183
+ def worker.retries_exhausted(*args)
184
+ raise 'bam!'
185
+ end
186
+
187
+ e = task_misbehaving_worker
188
+ assert_equal e.message, "kerblammo!"
189
+ worker.verify
190
+ end
191
+
192
+ def task_misbehaving_worker
193
+ assert_raises RuntimeError do
194
+ handler.call(worker, msg, 'default') do
195
+ raise 'kerblammo!'
196
+ end
197
+ end
198
+ end
199
+ end
170
200
  end
171
201
 
172
202
  describe 'poller' do
@@ -18,20 +18,20 @@ class TestScheduling < MiniTest::Unit::TestCase
18
18
  end
19
19
 
20
20
  it 'schedules a job via interval' do
21
- @redis.expect :zadd, true, ['schedule', String, String]
21
+ @redis.expect :zadd, true, ['schedule', Array]
22
22
  assert ScheduledWorker.perform_in(600, 'mike')
23
23
  @redis.verify
24
24
  end
25
25
 
26
26
  it 'schedules a job via timestamp' do
27
- @redis.expect :zadd, true, ['schedule', String, String]
27
+ @redis.expect :zadd, true, ['schedule', Array]
28
28
  assert ScheduledWorker.perform_in(5.days.from_now, 'mike')
29
29
  @redis.verify
30
30
  end
31
31
 
32
32
  it 'schedules multiple jobs at once' do
33
33
  @redis.expect :zadd, true, ['schedule', Array]
34
- assert Sidekiq::Client.push_bulk('class' => ScheduledWorker, 'args' => ['mike', 'mike'], 'at' => 600)
34
+ assert Sidekiq::Client.push_bulk('class' => ScheduledWorker, 'args' => [['mike'], ['mike']], 'at' => 600)
35
35
  @redis.verify
36
36
  end
37
37
  end
@@ -1,3 +1,4 @@
1
+ # encoding: utf-8
1
2
  require 'helper'
2
3
 
3
4
  class TestSidekiq < MiniTest::Unit::TestCase
@@ -24,4 +25,13 @@ class TestSidekiq < MiniTest::Unit::TestCase
24
25
  end
25
26
  end
26
27
 
28
+ describe "❨╯°□°❩╯︵┻━┻" do
29
+ before { $stdout = StringIO.new }
30
+ after { $stdout = STDOUT }
31
+
32
+ it "allows angry developers to express their emotional constitution and remedies it" do
33
+ Sidekiq.❨╯°□°❩╯︵┻━┻
34
+ assert_equal "Calm down, bro\n", $stdout.string
35
+ end
36
+ end
27
37
  end
@@ -53,10 +53,10 @@ class TestTesting < MiniTest::Unit::TestCase
53
53
 
54
54
  after do
55
55
  # Undo override
56
- Sidekiq::Worker::ClassMethods.class_eval do
57
- remove_method :client_push
58
- alias_method :client_push, :client_push_old
59
- remove_method :client_push_old
56
+ (class << Sidekiq::Client; self; end).class_eval do
57
+ remove_method :raw_push
58
+ alias_method :raw_push, :raw_push_old
59
+ remove_method :raw_push_old
60
60
  end
61
61
  end
62
62
 
@@ -109,6 +109,14 @@ class TestTesting < MiniTest::Unit::TestCase
109
109
  StoredWorker.drain
110
110
  end
111
111
  assert_equal 0, StoredWorker.jobs.size
112
+
113
+ end
114
+
115
+ it 'round trip serializes the job arguments' do
116
+ assert StoredWorker.perform_async(:mike)
117
+ job = StoredWorker.jobs.first
118
+ assert_equal "mike", job['args'].first
119
+ StoredWorker.clear
112
120
  end
113
121
 
114
122
  class FirstWorker
@@ -81,10 +81,10 @@ class TestInline < MiniTest::Unit::TestCase
81
81
  end
82
82
 
83
83
  it 'stubs the push_bulk call when in testing mode' do
84
- assert Sidekiq::Client.push_bulk({'class' => InlineWorker, 'args' => [true, true]})
84
+ assert Sidekiq::Client.push_bulk({'class' => InlineWorker, 'args' => [[true], [true]]})
85
85
 
86
86
  assert_raises InlineError do
87
- Sidekiq::Client.push_bulk({'class' => InlineWorker, 'args' => [true, false]})
87
+ Sidekiq::Client.push_bulk({'class' => InlineWorker, 'args' => [[true], [false]]})
88
88
  end
89
89
  end
90
90
 
@@ -12,4 +12,4 @@
12
12
  li
13
13
  a href="#{url}?page=#{@current_page + 1}" #{@current_page + 1}
14
14
  li class="#{'disabled' if @total_size <= @current_page * @count}"
15
- a href="#{url}?page=#{(@total_size / @count).ceil + 1}" »
15
+ a href="#{url}?page=#{(@total_size.to_f / @count).ceil}" »
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sidekiq
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.8.0
4
+ version: 2.9.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2013-03-04 00:00:00.000000000 Z
12
+ date: 2013-03-28 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: redis
@@ -187,6 +187,22 @@ dependencies:
187
187
  - - ~>
188
188
  - !ruby/object:Gem::Version
189
189
  version: '3'
190
+ - !ruby/object:Gem::Dependency
191
+ name: coveralls
192
+ requirement: !ruby/object:Gem::Requirement
193
+ none: false
194
+ requirements:
195
+ - - ! '>='
196
+ - !ruby/object:Gem::Version
197
+ version: '0'
198
+ type: :development
199
+ prerelease: false
200
+ version_requirements: !ruby/object:Gem::Requirement
201
+ none: false
202
+ requirements:
203
+ - - ! '>='
204
+ - !ruby/object:Gem::Version
205
+ version: '0'
190
206
  description: Simple, efficient message processing for Ruby
191
207
  email:
192
208
  - mperham@gmail.com
@@ -204,7 +220,6 @@ files:
204
220
  - LICENSE
205
221
  - README.md
206
222
  - Rakefile
207
- - bin/client
208
223
  - bin/sidekiq
209
224
  - bin/sidekiqctl
210
225
  - config.ru
@@ -258,7 +273,6 @@ files:
258
273
  - test/test_retry.rb
259
274
  - test/test_scheduling.rb
260
275
  - test/test_sidekiq.rb
261
- - test/test_stats.rb
262
276
  - test/test_testing.rb
263
277
  - test/test_testing_inline.rb
264
278
  - test/test_web.rb
@@ -327,7 +341,6 @@ test_files:
327
341
  - test/test_retry.rb
328
342
  - test/test_scheduling.rb
329
343
  - test/test_sidekiq.rb
330
- - test/test_stats.rb
331
344
  - test/test_testing.rb
332
345
  - test/test_testing_inline.rb
333
346
  - test/test_web.rb
data/bin/client DELETED
@@ -1,7 +0,0 @@
1
- #!/usr/bin/env ruby
2
-
3
- require 'sidekiq'
4
-
5
- 10.times do
6
- Sidekiq::Client.push('class' => 'HardWorker', 'args' => ['bob', 1])
7
- end
@@ -1,47 +0,0 @@
1
- require 'helper'
2
- require 'sidekiq'
3
- require 'sidekiq/processor'
4
-
5
- class TestStats < MiniTest::Unit::TestCase
6
- describe 'with redis' do
7
- before do
8
- @redis = Sidekiq.redis = REDIS
9
- Sidekiq.redis {|c| c.flushdb }
10
- end
11
-
12
- class DumbWorker
13
- include Sidekiq::Worker
14
- sidekiq_options :queue => 'dumbq'
15
-
16
- def perform(arg)
17
- raise 'bang' if arg == nil
18
- end
19
- end
20
-
21
- describe "info counts" do
22
- before do
23
- @redis.with do |conn|
24
- conn.rpush 'queue:foo', '{}'
25
- conn.sadd 'queues', 'foo'
26
-
27
- 3.times { conn.rpush 'queue:bar', '{}' }
28
- conn.sadd 'queues', 'bar'
29
-
30
- 2.times { conn.rpush 'queue:baz', '{}' }
31
- conn.sadd 'queues', 'baz'
32
- end
33
- end
34
-
35
- describe "size" do
36
- it "returns size of queues" do
37
- assert_equal 0, Sidekiq.size("foox")
38
- assert_equal 1, Sidekiq.size(:foo)
39
- assert_equal 1, Sidekiq.size("foo")
40
- assert_equal 4, Sidekiq.size("foo", "bar")
41
- assert_equal 6, Sidekiq.size
42
- end
43
- end
44
- end
45
-
46
- end
47
- end