sidekiq 0.9.1 → 0.10.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/.gitignore CHANGED
@@ -3,3 +3,4 @@ Gemfile.lock
3
3
  *.swp
4
4
  dump.rdb
5
5
  .rbx
6
+ coverage/
data/Changes.md CHANGED
@@ -1,3 +1,12 @@
1
+ 0.10.0
2
+ -----------
3
+
4
+ - Reworked capistrano recipe to make it more fault-tolerant [#94].
5
+ - Automatic failure retry! Sidekiq will now save failed messages
6
+ and retry them, with an exponential backoff, over about 20 days.
7
+ Did a message fail to process? Just deploy a bug fix in the next
8
+ few days and Sidekiq will retry the message eventually.
9
+
1
10
  0.9.1
2
11
  -----------
3
12
 
data/README.md CHANGED
@@ -3,14 +3,12 @@ Sidekiq
3
3
 
4
4
  Simple, efficient message processing for Ruby.
5
5
 
6
- Sidekiq aims to be compatible with Resque. It uses the exact same
6
+ Sidekiq is compatible with Resque. It uses the exact same
7
7
  message format as Resque so it can integrate into an existing Resque processing farm.
8
8
  You can have Sidekiq and Resque run side-by-side at the same time and
9
9
  use the Resque client to enqueue messages in Redis to be processed by Sidekiq.
10
10
 
11
- Sidekiq is different from Resque in how it processes messages: it
12
- processes many messages concurrently per process. Resque only processes
13
- one message at a time per process so it is far less memory efficient.
11
+ At the same time, Sidekiq uses multithreading so it much more memory efficient than Resque (which forks a new process for every job).
14
12
  You'll find that you might need 50 200MB resque processes to peg your CPU
15
13
  whereas one 300MB Sidekiq process will peg the same CPU and perform the
16
14
  same amount of work. Please see [my blog post on Resque's memory
@@ -18,6 +16,8 @@ efficiency](http://blog.carbonfive.com/2011/09/16/improving-resques-memory-effic
18
16
  and how I was able to shrink a Carbon Five client's resque processing farm
19
17
  from 9 machines to 1 machine.
20
18
 
19
+ In sum, if your jobs are well-behaved and threadsafe, Sidekiq is probably a good replacement for Resque. If your jobs are not thread-safe or they leak memory, you may want to continue using Resque, because its forking model gives you more protection.
20
+
21
21
 
22
22
  Requirements
23
23
  -----------------
data/Rakefile CHANGED
@@ -1,7 +1,8 @@
1
1
  require 'rake/testtask'
2
2
  Rake::TestTask.new(:test) do |test|
3
3
  test.libs << 'test'
4
- test.warning = true
4
+ #SO MUCH NOISE
5
+ #test.warning = true
5
6
  test.pattern = 'test/**/test_*.rb'
6
7
  end
7
8
 
data/TODO.md CHANGED
@@ -1 +1 @@
1
- - reschedule failed tasks
1
+ - Make the Web UI less ugly
@@ -0,0 +1,43 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'fileutils'
4
+
5
+ stage = ARGV[0]
6
+ pidfile = ARGV[1]
7
+ timeout = ARGV[2].to_i
8
+ timeout = 10 if timeout == 0
9
+
10
+ def done(msg)
11
+ puts msg
12
+ exit(0)
13
+ end
14
+
15
+ done 'No pidfile given' if !pidfile
16
+ done 'Pidfile does not exist' if !File.exist?(pidfile)
17
+
18
+ pid = File.read(pidfile).to_i
19
+ done 'Invalid pidfile content' if pid == 0
20
+
21
+ begin
22
+ Process.getpgid(pid)
23
+ rescue Errno::ESRCH
24
+ done "Process doesn't exist"
25
+ end
26
+
27
+ case stage
28
+ when 'quiet'
29
+ `kill -USR1 #{pid}`
30
+ when 'stop'
31
+ `kill -TERM #{pid}`
32
+ timeout.times do
33
+ begin
34
+ Process.getpgid(pid)
35
+ rescue Errno::ESRCH
36
+ FileUtils.rm_f pidfile
37
+ done 'Sidekiq shut down gracefully.'
38
+ end
39
+ sleep 1
40
+ end
41
+ `kill -9 #{pid}`
42
+ done 'Sidekiq shut down forcefully.'
43
+ end
@@ -5,3 +5,7 @@ Sidekiq is a Redis-backed Ruby library for creating background jobs, placing tho
5
5
  = USAGE:
6
6
 
7
7
  add require_recipe "sidekiq" to main/recipes/default.rb
8
+
9
+ = NOTES:
10
+
11
+ I setup a basic size for the Sidekiq workers based on the instance_type, if you need more or less workers please modify the recipe itself.
@@ -2,7 +2,8 @@
2
2
  # Cookbook Name:: sidekiq
3
3
  # Recipe:: default
4
4
  #
5
- if ['solo', 'util'].include?(node[:instance_role])
5
+ role = node[:instance_role]
6
+ if role == 'solo' || (role == 'util' && node[:name] =~ /sidekiq/)
6
7
 
7
8
  # for now
8
9
  worker_count = 1
@@ -140,13 +140,13 @@ APP_SHARED="${APP_DIR}/shared"
140
140
  APP_CONFIG="${APP_SHARED}/config"
141
141
 
142
142
  if [ -e "${APP_CONFIG}/${CONF_FILE}" ]; then
143
- logger -t "sidekiq_${APP}" -s "Good, found a config file. Proceeding..."
143
+ logger -t "sidekiq_${APP}" -s "Good, found a conf file. Proceeding..."
144
144
  else
145
145
  logger -t "sidekiq_${APP}" -s "/data/${APP}/shared/config/${CONF_FILE} not found for app: ${APP}"
146
146
  exit 1
147
147
  fi
148
148
 
149
- WORKER_REF=`echo $CONF_FILE | sed s/.yml.conf//`
149
+ WORKER_REF=`echo $CONF_FILE | sed s/.conf//`
150
150
  LOG_FILE="$APP_ROOT/log/$WORKER_REF.log"
151
151
  LOCK_FILE="/tmp/$WORKER_REF.monit-lock"
152
152
  PID_FILE="/var/run/engineyard/sidekiq/$APP/$WORKER_REF.pid"
@@ -2,12 +2,6 @@
2
2
  <% if @verbose %>
3
3
  :verbose: <%= @verbose %>
4
4
  <% end %>
5
- <% if @namespace %>
6
- :namespace: <%= @namespace %>
7
- <% end %>
8
- <% if @server %>
9
- :server: <%= @server %>
10
- <% end %>
11
5
  <% if @environment %>
12
6
  :environment: <%= @environment %>
13
7
  <% end %>
@@ -2,12 +2,12 @@ require 'sidekiq'
2
2
 
3
3
  # If your client is single-threaded, we just need a single connection in our Redis connection pool
4
4
  Sidekiq.configure_client do |config|
5
- config.redis = Sidekiq::RedisConnection.create(:namespace => 'x', :size => 1, :url => 'redis://redis.host:1234/db')
5
+ config.redis = Sidekiq::RedisConnection.create(:namespace => 'x', :size => 1, :url => 'redis://redis.host:1234/14')
6
6
  end
7
7
 
8
8
  # Sidekiq server is multi-threaded so our Redis connection pool size defaults to concurrency (-c)
9
9
  Sidekiq.configure_server do |config|
10
- config.redis = Sidekiq::RedisConnection.create(:namespace => 'x', :url => 'redis://redis.host:1234/db')
10
+ config.redis = Sidekiq::RedisConnection.create(:namespace => 'x', :url => 'redis://redis.host:1234/14')
11
11
  end
12
12
 
13
13
  # Start up sidekiq via
@@ -12,7 +12,7 @@ class SinatraWorker
12
12
  include Sidekiq::Worker
13
13
 
14
14
  def perform(msg="lulz you forgot a msg!")
15
- $redis.lpush("sinkiq-example-messages", msg)
15
+ $redis.lpush("sinkiq-example-messages", msg)
16
16
  end
17
17
  end
18
18
 
Binary file
@@ -16,6 +16,7 @@ module Sidekiq
16
16
  :require => '.',
17
17
  :environment => nil,
18
18
  :timeout => 5,
19
+ :enable_rails_extensions => true,
19
20
  }
20
21
 
21
22
  def self.options
@@ -2,18 +2,18 @@ Capistrano::Configuration.instance.load do
2
2
  before "deploy", "sidekiq:quiet"
3
3
  after "deploy", "sidekiq:restart"
4
4
 
5
- _cset(:sidekiq_timeout) { 5 }
5
+ _cset(:sidekiq_timeout) { 10 }
6
6
 
7
7
  namespace :sidekiq do
8
8
 
9
9
  desc "Quiet sidekiq (stop accepting new work)"
10
10
  task :quiet do
11
- run "cd #{current_path} && kill -USR1 `cat #{current_path}/tmp/pids/sidekiq.pid`"
11
+ run "cd #{current_path} && sidekiqctl quiet #{current_path}/tmp/pids/sidekiq.pid"
12
12
  end
13
13
 
14
14
  desc "Stop sidekiq"
15
15
  task :stop do
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"
16
+ run "cd #{current_path} && sidekiqctl stop #{current_path}/tmp/pids/sidekiq.pid #{fetch :sidekiq_timeout}"
17
17
  end
18
18
 
19
19
  desc "Start sidekiq"
@@ -28,11 +28,11 @@ module Sidekiq
28
28
  include Singleton
29
29
 
30
30
  # Used for CLI testing
31
- attr_accessor :code, :manager
31
+ attr_accessor :code
32
+ attr_accessor :manager
32
33
 
33
34
  def initialize
34
35
  @code = nil
35
- @manager = nil
36
36
  end
37
37
 
38
38
  def parse(args=ARGV)
@@ -44,6 +44,7 @@ module Sidekiq
44
44
  options.merge!(config.merge(cli))
45
45
 
46
46
  Sidekiq::Util.logger.level = Logger::DEBUG if options[:verbose]
47
+ Celluloid.logger = nil
47
48
 
48
49
  validate!
49
50
  write_pid
@@ -52,14 +53,17 @@ module Sidekiq
52
53
 
53
54
  def run
54
55
  @manager = Sidekiq::Manager.new(options)
56
+ poller = Sidekiq::Retry::Poller.new
55
57
  begin
56
58
  logger.info 'Starting processing, hit Ctrl-C to stop'
57
- manager.start!
59
+ @manager.start!
60
+ poller.poll!
58
61
  sleep
59
62
  rescue Interrupt
60
63
  logger.info 'Shutting down'
61
- manager.stop!(:shutdown => true, :timeout => options[:timeout])
62
- manager.wait(:shutdown)
64
+ poller.terminate
65
+ @manager.stop!(:shutdown => true, :timeout => options[:timeout])
66
+ @manager.wait(:shutdown)
63
67
  end
64
68
  end
65
69
 
@@ -122,7 +122,7 @@ module Sidekiq
122
122
  @ready.size.times do
123
123
  found ||= find_work(@queues.sample)
124
124
  end
125
- break logger.debug('nothing to process') unless found
125
+ break unless found
126
126
  end
127
127
 
128
128
  # This is the polling loop that ensures we check Redis every
@@ -64,6 +64,10 @@ module Sidekiq
64
64
  entries.map(&:make_new)
65
65
  end
66
66
 
67
+ def clear
68
+ entries.clear
69
+ end
70
+
67
71
  def invoke(*args, &final_action)
68
72
  chain = retrieve.dup
69
73
  traverse_chain = lambda do
@@ -9,13 +9,23 @@ module Sidekiq
9
9
 
10
10
  def call(item, queue)
11
11
  payload_hash = Digest::MD5.hexdigest(MultiJson.encode(item))
12
+ unique = false
13
+
12
14
  Sidekiq.redis do |conn|
13
- return if conn.get(payload_hash)
14
- conn.setex(payload_hash, HASH_KEY_EXPIRATION, 1)
15
+ conn.watch(payload_hash)
16
+
17
+ if conn.get(payload_hash)
18
+ conn.unwatch
19
+ else
20
+ unique = conn.multi do
21
+ conn.setex(payload_hash, HASH_KEY_EXPIRATION, 1)
22
+ end
23
+ end
15
24
  end
16
25
 
17
- yield
26
+ yield if unique
18
27
  end
28
+
19
29
  end
20
30
  end
21
31
  end
@@ -0,0 +1,59 @@
1
+ require 'sidekiq/retry'
2
+
3
+ module Sidekiq
4
+ module Middleware
5
+ module Server
6
+ ##
7
+ # Automatically retry jobs that fail in Sidekiq.
8
+ # A message looks like:
9
+ #
10
+ # { 'class' => 'HardWorker', 'args' => [1, 2, 'foo'] }
11
+ #
12
+ # We'll add a bit more data to the message to support retries:
13
+ #
14
+ # * 'queue' - the queue to use
15
+ # * 'retry_count' - number of times we've retried so far.
16
+ # * 'error_message' - the message from the exception
17
+ # * 'error_class' - the exception class
18
+ # * 'failed_at' - the first time it failed
19
+ # * 'retried_at' - the last time it was retried
20
+ #
21
+ # We don't store the backtrace as that can add a lot of overhead
22
+ # to the message and everyone is using Airbrake, right?
23
+ class RetryJobs
24
+ include Sidekiq::Util
25
+ include Sidekiq::Retry
26
+
27
+ def call(worker, msg, queue)
28
+ yield
29
+ rescue => e
30
+ msg['queue'] = queue
31
+ msg['error_message'] = e.message
32
+ msg['error_class'] = e.class.name
33
+ count = if msg['retry_count']
34
+ msg['retried_at'] = Time.now.utc
35
+ msg['retry_count'] += 1
36
+ else
37
+ msg['failed_at'] = Time.now.utc
38
+ msg['retry_count'] = 0
39
+ end
40
+
41
+ if count <= MAX_COUNT
42
+ delay = DELAY.call(count)
43
+ logger.debug { "Failure! Retry #{count} in #{delay} seconds" }
44
+ retry_at = Time.now.to_f + delay
45
+ payload = MultiJson.encode(msg)
46
+ Sidekiq.redis do |conn|
47
+ conn.zadd('retry', retry_at.to_s, payload)
48
+ end
49
+ else
50
+ # Goodbye dear message, you (re)tried your best I'm sure.
51
+ logger.debug { "Dropping message after hitting the retry maximum: #{msg}" }
52
+ end
53
+ raise
54
+ end
55
+
56
+ end
57
+ end
58
+ end
59
+ end
@@ -5,7 +5,9 @@ module Sidekiq
5
5
  def call(*args)
6
6
  yield
7
7
  ensure
8
- Sidekiq.redis {|conn| conn.del(Digest::MD5.hexdigest(MultiJson.encode(args[1]))) }
8
+ json = MultiJson.encode(args[1])
9
+ hash = Digest::MD5.hexdigest(json)
10
+ Sidekiq.redis {|conn| conn.del(hash) }
9
11
  end
10
12
  end
11
13
  end
@@ -4,7 +4,7 @@ require 'sidekiq/util'
4
4
  require 'sidekiq/middleware/server/active_record'
5
5
  require 'sidekiq/middleware/server/exception_handler'
6
6
  require 'sidekiq/middleware/server/unique_jobs'
7
- require 'sidekiq/middleware/server/failure_jobs'
7
+ require 'sidekiq/middleware/server/retry_jobs'
8
8
  require 'sidekiq/middleware/server/logging'
9
9
 
10
10
  module Sidekiq
@@ -21,6 +21,7 @@ module Sidekiq
21
21
  m.add Middleware::Server::ExceptionHandler
22
22
  m.add Middleware::Server::Logging
23
23
  m.add Middleware::Server::UniqueJobs
24
+ m.add Middleware::Server::RetryJobs
24
25
  m.add Middleware::Server::ActiveRecord
25
26
  end
26
27
  end
@@ -1,5 +1,6 @@
1
1
  module Sidekiq
2
2
  def self.hook_rails!
3
+ return unless Sidekiq.options[:enable_rails_extensions]
3
4
  if defined?(ActiveRecord)
4
5
  ActiveRecord::Base.extend(Sidekiq::Extensions::ActiveRecord)
5
6
  ActiveRecord::Base.send(:include, Sidekiq::Extensions::ActiveRecord)
@@ -0,0 +1,57 @@
1
+ require 'sidekiq'
2
+ require 'celluloid'
3
+
4
+ module Sidekiq
5
+ ##
6
+ # Sidekiq's retry support assumes a typical development lifecycle:
7
+ # 0. push some code changes with a bug in it
8
+ # 1. bug causes message processing to fail, sidekiq's middleware captures
9
+ # the message and pushes it onto a retry queue
10
+ # 2. sidekiq retries messages in the retry queue multiple times with
11
+ # an exponential delay, the message continues to fail
12
+ # 3. after a few days, a developer deploys a fix. the message is
13
+ # reprocessed successfully.
14
+ # 4. if 3 never happens, sidekiq will eventually give up and throw the
15
+ # message away.
16
+ module Retry
17
+
18
+ # delayed_job uses the same basic formula
19
+ MAX_COUNT = 25
20
+ DELAY = proc { |count| (count ** 4) + 15 }
21
+ POLL_INTERVAL = 15
22
+
23
+ ##
24
+ # The Poller checks Redis every N seconds for messages in the retry
25
+ # set have passed their retry timestamp and should be retried. If so, it
26
+ # just pops the message back onto its original queue so the
27
+ # workers can pick it up like any other message.
28
+ class Poller
29
+ include Celluloid
30
+ include Sidekiq::Util
31
+
32
+ def poll
33
+ watchdog('retry poller thread died!') do
34
+
35
+ Sidekiq.redis do |conn|
36
+ # A message's "score" in Redis is the time at which it should be retried.
37
+ # Just check Redis for the set of messages with a timestamp before now.
38
+ messages = nil
39
+ now = Time.now.to_f.to_s
40
+ (messages, _) = conn.multi do
41
+ conn.zrangebyscore('retry', '-inf', now)
42
+ conn.zremrangebyscore('retry', '-inf', now)
43
+ end
44
+
45
+ messages.each do |message|
46
+ logger.debug { "Retrying #{message}" }
47
+ msg = MultiJson.decode(message)
48
+ conn.rpush("queue:#{msg['queue']}", message)
49
+ end
50
+ end
51
+
52
+ after(POLL_INTERVAL) { poll }
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -1,3 +1,3 @@
1
1
  module Sidekiq
2
- VERSION = "0.9.1"
2
+ VERSION = "0.10.0"
3
3
  end
@@ -56,6 +56,17 @@ module Sidekiq
56
56
  Sidekiq.redis { |conn| conn.get('stat:failed') } || 0
57
57
  end
58
58
 
59
+ def retry_count
60
+ Sidekiq.redis { |conn| conn.zcard('retry') }
61
+ end
62
+
63
+ def retries
64
+ Sidekiq.redis do |conn|
65
+ results = conn.zrange('retry', 0, 25, :withscores => true)
66
+ results.each_slice(2).map { |msg, score| [MultiJson.decode(msg), Float(score)] }
67
+ end
68
+ end
69
+
59
70
  def queues
60
71
  Sidekiq.redis do |conn|
61
72
  conn.smembers('queues').map do |q|
@@ -19,6 +19,11 @@ class WorkController < ApplicationController
19
19
  render :text => 'enqueued'
20
20
  end
21
21
 
22
+ def crash
23
+ HardWorker.perform_async('crash', 1, Time.now.to_f)
24
+ render :text => 'enqueued'
25
+ end
26
+
22
27
  def delayed_post
23
28
  p = Post.first
24
29
  unless p
@@ -2,6 +2,7 @@ class HardWorker
2
2
  include Sidekiq::Worker
3
3
 
4
4
  def perform(name, count, salt)
5
+ raise name if name == 'crash'
5
6
  print "#{Time.now}\n"
6
7
  sleep count
7
8
  end
@@ -6,4 +6,5 @@ Myapp::Application.routes.draw do
6
6
  get "work/email" => "work#email"
7
7
  get "work/post" => "work#delayed_post"
8
8
  get "work/long" => "work#long"
9
+ get "work/crash" => "work#crash"
9
10
  end
@@ -7,7 +7,7 @@ Gem::Specification.new do |gem|
7
7
  gem.description = gem.summary = "Simple, efficient message processing for Ruby"
8
8
  gem.homepage = "http://mperham.github.com/sidekiq"
9
9
 
10
- gem.executables = ['sidekiq']
10
+ gem.executables = ['sidekiq', 'sidekiqctl']
11
11
  gem.files = `git ls-files`.split("\n")
12
12
  gem.test_files = `git ls-files -- test/*`.split("\n")
13
13
  gem.name = "sidekiq"
@@ -15,13 +15,13 @@ Gem::Specification.new do |gem|
15
15
  gem.version = Sidekiq::VERSION
16
16
  gem.add_dependency 'redis'
17
17
  gem.add_dependency 'redis-namespace'
18
- gem.add_dependency 'connection_pool', '>= 0.9.0'
18
+ gem.add_dependency 'connection_pool', '~> 0.9.0'
19
19
  gem.add_dependency 'celluloid'
20
20
  gem.add_dependency 'multi_json'
21
21
  gem.add_development_dependency 'minitest'
22
22
  gem.add_development_dependency 'sinatra'
23
23
  gem.add_development_dependency 'slim'
24
24
  gem.add_development_dependency 'rake'
25
- gem.add_development_dependency 'actionmailer'
26
- gem.add_development_dependency 'activerecord'
25
+ gem.add_development_dependency 'actionmailer', '~> 3'
26
+ gem.add_development_dependency 'activerecord', '~> 3'
27
27
  end
@@ -1,5 +1,4 @@
1
- $TESTING = true
2
- if false
1
+ if ENV.has_key?("SIMPLECOV")
3
2
  require 'simplecov'
4
3
  SimpleCov.start
5
4
  end
@@ -13,4 +12,4 @@ require 'sidekiq/util'
13
12
  Sidekiq::Util.logger.level = Logger::ERROR
14
13
 
15
14
  require 'sidekiq/redis_connection'
16
- REDIS = Sidekiq::RedisConnection.create(:url => 'redis://localhost/sidekiq_test')
15
+ REDIS = Sidekiq::RedisConnection.create(:url => "redis://localhost/15")
@@ -28,17 +28,19 @@ class TestClient < MiniTest::Unit::TestCase
28
28
  describe 'with mock redis' do
29
29
  before do
30
30
  @redis = MiniTest::Mock.new
31
- def @redis.multi; yield; end
31
+ def @redis.multi; yield if block_given?; end
32
32
  def @redis.set(*); true; end
33
33
  def @redis.sadd(*); true; end
34
34
  def @redis.srem(*); true; end
35
35
  def @redis.get(*); nil; end
36
36
  def @redis.del(*); nil; end
37
37
  def @redis.incrby(*); nil; end
38
- def @redis.setex(*); nil; end
38
+ def @redis.setex(*); true; end
39
39
  def @redis.expire(*); true; end
40
+ def @redis.watch(*); true; end
40
41
  def @redis.with_connection; yield self; end
41
42
  def @redis.with; yield self; end
43
+ def @redis.exec; true; end
42
44
  Sidekiq.instance_variable_set(:@redis, @redis)
43
45
  end
44
46
 
@@ -28,10 +28,6 @@ class TestExtensions < MiniTest::Unit::TestCase
28
28
  assert_equal 1, Sidekiq.redis.llen('queue:default')
29
29
  end
30
30
 
31
- it 'allows delayed exection of ActiveRecord instance methods' do
32
- skip('requires a database')
33
- end
34
-
35
31
  class UserMailer < ActionMailer::Base
36
32
  def greetings(a, b)
37
33
  raise "Should not be called!"
@@ -45,6 +41,28 @@ class TestExtensions < MiniTest::Unit::TestCase
45
41
  assert_equal ['default'], Sidekiq::Client.registered_queues
46
42
  assert_equal 1, Sidekiq.redis.llen('queue:default')
47
43
  end
44
+ end
48
45
 
46
+ describe 'sidekiq rails extensions configuration' do
47
+ before do
48
+ @options = Sidekiq.options
49
+ end
50
+
51
+ after do
52
+ Sidekiq.options = @options
53
+ end
54
+
55
+ it 'should set enable_rails_extensions option to true by default' do
56
+ assert Sidekiq.options[:enable_rails_extensions]
57
+ end
58
+
59
+ it 'should extend ActiveRecord and ActiveMailer if enable_rails_extensions is true' do
60
+ assert Sidekiq.hook_rails!
61
+ end
62
+
63
+ it 'should not extend ActiveRecord and ActiveMailer if enable_rails_extensions is false' do
64
+ Sidekiq.options = { :enable_rails_extensions => false }
65
+ refute Sidekiq.hook_rails!
66
+ end
49
67
  end
50
68
  end
@@ -6,9 +6,22 @@ require 'sidekiq/processor'
6
6
  class TestMiddleware < MiniTest::Unit::TestCase
7
7
  describe 'middleware chain' do
8
8
  before do
9
+ $errors = []
9
10
  Sidekiq.redis = REDIS
10
11
  end
11
12
 
13
+ it 'handles errors' do
14
+ handler = Sidekiq::Middleware::Server::ExceptionHandler.new
15
+
16
+ assert_raises ArgumentError do
17
+ handler.call('', { :a => 1 }, 'default') do
18
+ raise ArgumentError
19
+ end
20
+ end
21
+ assert_equal 1, $errors.size
22
+ assert_equal({ :a => 1 }, $errors[0][:parameters])
23
+ end
24
+
12
25
  class CustomMiddleware
13
26
  def initialize(name, recorder)
14
27
  @name = name
@@ -42,10 +55,6 @@ class TestMiddleware < MiniTest::Unit::TestCase
42
55
  end
43
56
 
44
57
  it 'executes middleware in the proper order' do
45
- Sidekiq::Middleware::Server::UniqueJobs.class_eval do
46
- def call(*args); yield; end
47
- end
48
-
49
58
  recorder = []
50
59
  msg = { 'class' => CustomWorker.to_s, 'args' => [recorder] }
51
60
 
@@ -74,3 +83,10 @@ class TestMiddleware < MiniTest::Unit::TestCase
74
83
  end
75
84
  end
76
85
  end
86
+
87
+ class FakeAirbrake
88
+ def self.notify(ex, hash)
89
+ $errors << hash
90
+ end
91
+ end
92
+ Airbrake = FakeAirbrake
@@ -8,6 +8,7 @@ class TestProcessor < MiniTest::Unit::TestCase
8
8
  $errors = []
9
9
  @boss = MiniTest::Mock.new
10
10
  Celluloid.logger = nil
11
+ Sidekiq.redis = REDIS
11
12
  end
12
13
 
13
14
  class MockWorker
@@ -27,26 +28,5 @@ class TestProcessor < MiniTest::Unit::TestCase
27
28
  assert_equal 1, $invokes
28
29
  assert_equal 0, $errors.size
29
30
  end
30
-
31
- it 'handles exceptions' do
32
- msg = { 'class' => MockWorker.to_s, 'args' => ['boom'] }
33
- processor = ::Sidekiq::Processor.new(@boss)
34
- assert_raises RuntimeError do
35
- processor.process(msg, 'default')
36
- end
37
- @boss.verify
38
- assert_equal 0, $invokes
39
- assert_equal 1, $errors.size
40
- assert_equal msg, $errors[0][:parameters]
41
- end
42
-
43
31
  end
44
32
  end
45
-
46
- class FakeAirbrake
47
- def self.notify(ex, hash)
48
- $errors << hash
49
- end
50
- end
51
- Airbrake = FakeAirbrake
52
-
@@ -0,0 +1,83 @@
1
+ require 'helper'
2
+ require 'sidekiq/retry'
3
+ require 'sidekiq/middleware/server/retry_jobs'
4
+
5
+ class TestRetry < MiniTest::Unit::TestCase
6
+ describe 'middleware' do
7
+ before do
8
+ @redis = MiniTest::Mock.new
9
+ # Ugh, this is terrible.
10
+ Sidekiq.instance_variable_set(:@redis, @redis)
11
+
12
+ def @redis.with; yield self; end
13
+ end
14
+
15
+ it 'handles a new failed message' do
16
+ @redis.expect :zadd, 1, ['retry', String, String]
17
+ msg = { 'class' => 'Bob', 'args' => [1,2,'foo'] }
18
+ handler = Sidekiq::Middleware::Server::RetryJobs.new
19
+ assert_raises RuntimeError do
20
+ handler.call('', msg, 'default') do
21
+ raise "kerblammo!"
22
+ end
23
+ end
24
+ assert_equal 'default', msg["queue"]
25
+ assert_equal 'kerblammo!', msg["error_message"]
26
+ assert_equal 'RuntimeError', msg["error_class"]
27
+ assert_equal 0, msg["retry_count"]
28
+ assert msg["failed_at"]
29
+ @redis.verify
30
+ end
31
+
32
+ it 'handles a recurring failed message' do
33
+ @redis.expect :zadd, 1, ['retry', String, String]
34
+ now = Time.now.utc
35
+ msg = {"class"=>"Bob", "args"=>[1, 2, "foo"], "queue"=>"default", "error_message"=>"kerblammo!", "error_class"=>"RuntimeError", "failed_at"=>now, "retry_count"=>10}
36
+ handler = Sidekiq::Middleware::Server::RetryJobs.new
37
+ assert_raises RuntimeError do
38
+ handler.call('', msg, 'default') do
39
+ raise "kerblammo!"
40
+ end
41
+ end
42
+ assert_equal 'default', msg["queue"]
43
+ assert_equal 'kerblammo!', msg["error_message"]
44
+ assert_equal 'RuntimeError', msg["error_class"]
45
+ assert_equal 11, msg["retry_count"]
46
+ assert msg["failed_at"]
47
+ @redis.verify
48
+ end
49
+
50
+ it 'throws away old messages after too many retries' do
51
+ now = Time.now.utc
52
+ msg = {"class"=>"Bob", "args"=>[1, 2, "foo"], "queue"=>"default", "error_message"=>"kerblammo!", "error_class"=>"RuntimeError", "failed_at"=>now, "retry_count"=>25}
53
+ handler = Sidekiq::Middleware::Server::RetryJobs.new
54
+ assert_raises RuntimeError do
55
+ handler.call('', msg, 'default') do
56
+ raise "kerblammo!"
57
+ end
58
+ end
59
+ @redis.verify
60
+ end
61
+ end
62
+
63
+ describe 'poller' do
64
+ before do
65
+ @redis = MiniTest::Mock.new
66
+ Sidekiq.instance_variable_set(:@redis, @redis)
67
+
68
+ def @redis.with; yield self; end
69
+ end
70
+
71
+ it 'should poll like a bad mother...SHUT YO MOUTH' do
72
+ fake_msg = MultiJson.encode({ 'class' => 'Bob', 'args' => [1,2], 'queue' => 'someq' })
73
+ @redis.expect :multi, [[fake_msg], 1], []
74
+ @redis.expect :rpush, 1, ['queue:someq', fake_msg]
75
+
76
+ inst = Sidekiq::Retry::Poller.new
77
+ inst.poll
78
+
79
+ @redis.verify
80
+ end
81
+ end
82
+
83
+ end
@@ -4,6 +4,7 @@
4
4
  p Processed: #{processed}
5
5
  p Failed: #{failed}
6
6
  p Workers: #{workers.size}
7
+ p Retries Pending: #{retry_count}
7
8
 
8
9
  .tabbable
9
10
  ul.nav.nav-tabs
@@ -11,6 +12,8 @@
11
12
  a href="#workers" data-toggle="tab" Workers
12
13
  li
13
14
  a href="#queues" data-toggle="tab" Queues
15
+ li
16
+ a href="#retries" data-toggle="tab" Retries
14
17
  .tab-content
15
18
  #workers.tab-pane.active
16
19
  table class="table table-striped table-bordered"
@@ -40,3 +43,20 @@
40
43
  a href="queues/#{queue}" #{queue}
41
44
  td= size
42
45
 
46
+ #retries.tab-pane
47
+ table class="table table-striped table-bordered"
48
+ tr
49
+ th Next Retry
50
+ th Retry Count
51
+ th Queue
52
+ th Worker
53
+ th Args
54
+ - retries.each do |(msg, score)|
55
+ tr
56
+ td= Time.at(score)
57
+ td= msg['retry_count']
58
+ td
59
+ a href="queues/#{msg['queue']}" #{msg['queue']}
60
+ td= msg['class']
61
+ td= msg['args'].inspect[0..100]
62
+
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: 0.9.1
4
+ version: 0.10.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,11 +9,11 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-03-17 00:00:00.000000000 Z
12
+ date: 2012-03-28 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: redis
16
- requirement: &70238239105760 !ruby/object:Gem::Requirement
16
+ requirement: &70249655983600 !ruby/object:Gem::Requirement
17
17
  none: false
18
18
  requirements:
19
19
  - - ! '>='
@@ -21,10 +21,10 @@ dependencies:
21
21
  version: '0'
22
22
  type: :runtime
23
23
  prerelease: false
24
- version_requirements: *70238239105760
24
+ version_requirements: *70249655983600
25
25
  - !ruby/object:Gem::Dependency
26
26
  name: redis-namespace
27
- requirement: &70238239105320 !ruby/object:Gem::Requirement
27
+ requirement: &70249655982380 !ruby/object:Gem::Requirement
28
28
  none: false
29
29
  requirements:
30
30
  - - ! '>='
@@ -32,21 +32,21 @@ dependencies:
32
32
  version: '0'
33
33
  type: :runtime
34
34
  prerelease: false
35
- version_requirements: *70238239105320
35
+ version_requirements: *70249655982380
36
36
  - !ruby/object:Gem::Dependency
37
37
  name: connection_pool
38
- requirement: &70238239104820 !ruby/object:Gem::Requirement
38
+ requirement: &70249655981700 !ruby/object:Gem::Requirement
39
39
  none: false
40
40
  requirements:
41
- - - ! '>='
41
+ - - ~>
42
42
  - !ruby/object:Gem::Version
43
43
  version: 0.9.0
44
44
  type: :runtime
45
45
  prerelease: false
46
- version_requirements: *70238239104820
46
+ version_requirements: *70249655981700
47
47
  - !ruby/object:Gem::Dependency
48
48
  name: celluloid
49
- requirement: &70238239104400 !ruby/object:Gem::Requirement
49
+ requirement: &70249655981280 !ruby/object:Gem::Requirement
50
50
  none: false
51
51
  requirements:
52
52
  - - ! '>='
@@ -54,10 +54,10 @@ dependencies:
54
54
  version: '0'
55
55
  type: :runtime
56
56
  prerelease: false
57
- version_requirements: *70238239104400
57
+ version_requirements: *70249655981280
58
58
  - !ruby/object:Gem::Dependency
59
59
  name: multi_json
60
- requirement: &70238239103940 !ruby/object:Gem::Requirement
60
+ requirement: &70249655980820 !ruby/object:Gem::Requirement
61
61
  none: false
62
62
  requirements:
63
63
  - - ! '>='
@@ -65,10 +65,10 @@ dependencies:
65
65
  version: '0'
66
66
  type: :runtime
67
67
  prerelease: false
68
- version_requirements: *70238239103940
68
+ version_requirements: *70249655980820
69
69
  - !ruby/object:Gem::Dependency
70
70
  name: minitest
71
- requirement: &70238239103520 !ruby/object:Gem::Requirement
71
+ requirement: &70249655980400 !ruby/object:Gem::Requirement
72
72
  none: false
73
73
  requirements:
74
74
  - - ! '>='
@@ -76,10 +76,10 @@ dependencies:
76
76
  version: '0'
77
77
  type: :development
78
78
  prerelease: false
79
- version_requirements: *70238239103520
79
+ version_requirements: *70249655980400
80
80
  - !ruby/object:Gem::Dependency
81
81
  name: sinatra
82
- requirement: &70238239103100 !ruby/object:Gem::Requirement
82
+ requirement: &70249656007360 !ruby/object:Gem::Requirement
83
83
  none: false
84
84
  requirements:
85
85
  - - ! '>='
@@ -87,10 +87,10 @@ dependencies:
87
87
  version: '0'
88
88
  type: :development
89
89
  prerelease: false
90
- version_requirements: *70238239103100
90
+ version_requirements: *70249656007360
91
91
  - !ruby/object:Gem::Dependency
92
92
  name: slim
93
- requirement: &70238239102680 !ruby/object:Gem::Requirement
93
+ requirement: &70249656006940 !ruby/object:Gem::Requirement
94
94
  none: false
95
95
  requirements:
96
96
  - - ! '>='
@@ -98,10 +98,10 @@ dependencies:
98
98
  version: '0'
99
99
  type: :development
100
100
  prerelease: false
101
- version_requirements: *70238239102680
101
+ version_requirements: *70249656006940
102
102
  - !ruby/object:Gem::Dependency
103
103
  name: rake
104
- requirement: &70238239102260 !ruby/object:Gem::Requirement
104
+ requirement: &70249656006520 !ruby/object:Gem::Requirement
105
105
  none: false
106
106
  requirements:
107
107
  - - ! '>='
@@ -109,34 +109,35 @@ dependencies:
109
109
  version: '0'
110
110
  type: :development
111
111
  prerelease: false
112
- version_requirements: *70238239102260
112
+ version_requirements: *70249656006520
113
113
  - !ruby/object:Gem::Dependency
114
114
  name: actionmailer
115
- requirement: &70238239101840 !ruby/object:Gem::Requirement
115
+ requirement: &70249656006020 !ruby/object:Gem::Requirement
116
116
  none: false
117
117
  requirements:
118
- - - ! '>='
118
+ - - ~>
119
119
  - !ruby/object:Gem::Version
120
- version: '0'
120
+ version: '3'
121
121
  type: :development
122
122
  prerelease: false
123
- version_requirements: *70238239101840
123
+ version_requirements: *70249656006020
124
124
  - !ruby/object:Gem::Dependency
125
125
  name: activerecord
126
- requirement: &70238239101420 !ruby/object:Gem::Requirement
126
+ requirement: &70249656005520 !ruby/object:Gem::Requirement
127
127
  none: false
128
128
  requirements:
129
- - - ! '>='
129
+ - - ~>
130
130
  - !ruby/object:Gem::Version
131
- version: '0'
131
+ version: '3'
132
132
  type: :development
133
133
  prerelease: false
134
- version_requirements: *70238239101420
134
+ version_requirements: *70249656005520
135
135
  description: Simple, efficient message processing for Ruby
136
136
  email:
137
137
  - mperham@gmail.com
138
138
  executables:
139
139
  - sidekiq
140
+ - sidekiqctl
140
141
  extensions: []
141
142
  extra_rdoc_files: []
142
143
  files:
@@ -151,6 +152,7 @@ files:
151
152
  - TODO.md
152
153
  - bin/client
153
154
  - bin/sidekiq
155
+ - bin/sidekiqctl
154
156
  - config.ru
155
157
  - examples/chef/cookbooks/sidekiq/README.rdoc
156
158
  - examples/chef/cookbooks/sidekiq/recipes/default.rb
@@ -162,6 +164,7 @@ files:
162
164
  - examples/por.rb
163
165
  - examples/scheduling.rb
164
166
  - examples/sinkiq.rb
167
+ - examples/web-ui.png
165
168
  - lib/sidekiq.rb
166
169
  - lib/sidekiq/capistrano.rb
167
170
  - lib/sidekiq/cli.rb
@@ -176,10 +179,12 @@ files:
176
179
  - lib/sidekiq/middleware/server/exception_handler.rb
177
180
  - lib/sidekiq/middleware/server/failure_jobs.rb
178
181
  - lib/sidekiq/middleware/server/logging.rb
182
+ - lib/sidekiq/middleware/server/retry_jobs.rb
179
183
  - lib/sidekiq/middleware/server/unique_jobs.rb
180
184
  - lib/sidekiq/processor.rb
181
185
  - lib/sidekiq/rails.rb
182
186
  - lib/sidekiq/redis_connection.rb
187
+ - lib/sidekiq/retry.rb
183
188
  - lib/sidekiq/testing.rb
184
189
  - lib/sidekiq/util.rb
185
190
  - lib/sidekiq/version.rb
@@ -235,6 +240,7 @@ files:
235
240
  - test/test_manager.rb
236
241
  - test/test_middleware.rb
237
242
  - test/test_processor.rb
243
+ - test/test_retry.rb
238
244
  - test/test_stats.rb
239
245
  - test/test_testing.rb
240
246
  - test/test_web.rb
@@ -295,6 +301,7 @@ test_files:
295
301
  - test/test_manager.rb
296
302
  - test/test_middleware.rb
297
303
  - test/test_processor.rb
304
+ - test/test_retry.rb
298
305
  - test/test_stats.rb
299
306
  - test/test_testing.rb
300
307
  - test/test_web.rb