sidekiq 0.5.1 → 0.6.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 (47) hide show
  1. data/.gitignore +5 -0
  2. data/.rvmrc +2 -1
  3. data/Changes.md +11 -0
  4. data/LICENSE +11 -4
  5. data/README.md +1 -7
  6. data/TODO.md +0 -1
  7. data/bin/sidekiq +2 -0
  8. data/examples/por.rb +17 -0
  9. data/examples/sinkiq.rb +57 -0
  10. data/lib/sidekiq.rb +1 -1
  11. data/lib/sidekiq/cli.rb +72 -34
  12. data/lib/sidekiq/client.rb +33 -16
  13. data/lib/sidekiq/manager.rb +37 -47
  14. data/lib/sidekiq/middleware/chain.rb +92 -0
  15. data/lib/sidekiq/middleware/client/resque_web_compatibility.rb +17 -0
  16. data/lib/sidekiq/middleware/client/unique_jobs.rb +30 -0
  17. data/lib/sidekiq/middleware/server/active_record.rb +13 -0
  18. data/lib/sidekiq/middleware/server/airbrake.rb +30 -0
  19. data/lib/sidekiq/middleware/server/unique_jobs.rb +17 -0
  20. data/lib/sidekiq/processor.rb +74 -16
  21. data/lib/sidekiq/redis_connection.rb +23 -0
  22. data/lib/sidekiq/testing.rb +34 -0
  23. data/lib/sidekiq/util.rb +21 -12
  24. data/lib/sidekiq/version.rb +1 -1
  25. data/lib/sidekiq/worker.rb +6 -7
  26. data/myapp/Gemfile +4 -1
  27. data/myapp/Gemfile.lock +29 -6
  28. data/myapp/app/controllers/work_controller.rb +9 -0
  29. data/myapp/app/views/work/index.html.erb +1 -0
  30. data/myapp/app/workers/hard_worker.rb +4 -2
  31. data/myapp/config/environments/development.rb +1 -0
  32. data/myapp/config/initializers/sidekiq.rb +1 -0
  33. data/myapp/config/routes.rb +4 -56
  34. data/sidekiq.gemspec +1 -0
  35. data/test/fake_env.rb +0 -0
  36. data/test/helper.rb +3 -0
  37. data/test/test_cli.rb +49 -0
  38. data/test/test_client.rb +52 -10
  39. data/test/test_manager.rb +11 -6
  40. data/test/test_middleware.rb +39 -20
  41. data/test/test_processor.rb +3 -2
  42. data/test/test_stats.rb +79 -0
  43. data/test/test_testing.rb +32 -0
  44. metadata +47 -18
  45. data/Gemfile.lock +0 -32
  46. data/lib/sidekiq/middleware.rb +0 -89
  47. data/test/timed_queue.rb +0 -42
@@ -0,0 +1,5 @@
1
+ tags
2
+ Gemfile.lock
3
+ *.swp
4
+ dump.rdb
5
+ .rbx
data/.rvmrc CHANGED
@@ -1,3 +1,4 @@
1
- export RUBYOPT="-Ilib"
1
+ export RUBYOPT="-Ilib:test"
2
2
  export JRUBY_OPTS="--1.9"
3
3
  rvm use 1.9.3@sidekiq --create
4
+ #rvm use jruby@sidekiq --create
data/Changes.md CHANGED
@@ -1,3 +1,14 @@
1
+ 0.6.0
2
+ -----------
3
+
4
+ - Resque-compatible processing stats in redis (mperham)
5
+ - Simple client testing support in sidekiq/testing (mperham)
6
+ - Plain old Ruby support via the -r cli flag (mperham)
7
+ - Refactored middleware support, introducing ability to add client-side middleware (ryanlecompte)
8
+ - Added middleware for ignoring duplicate jobs (ryanlecompte)
9
+ - Added middleware for displaying jobs in resque-web dashboard (maxjustus)
10
+ - Added redis namespacing support (maxjustus)
11
+
1
12
  0.5.1
2
13
  -----------
3
14
 
data/LICENSE CHANGED
@@ -1,9 +1,16 @@
1
1
  Copyright (c) Mike Perham
2
2
 
3
- Sidekiq is open source licensed under the GPLv3 for non-commerical use:
3
+ You may choose to license Sidekiq in one of two ways:
4
+
5
+ - Open Source under the GPLv3
4
6
 
5
7
  Please see <http://www.gnu.org/licenses/gpl-3.0.html> for license text.
6
8
 
7
- To purchase a commercial license, you must pledge $50 to my Pledgie
8
- fund at http://www.pledgie.com/campaigns/16623. The commercial license can
9
- be found in COMM-LICENSE.
9
+ - Commercial License
10
+
11
+ The Sencha commercial software license in COMM-LICENSE. You must pledge
12
+ $50 to my Pledgie fund at http://www.pledgie.com/campaigns/16623 to use
13
+ this license.
14
+
15
+ This commercial license is just meant to help support my OSS work. If
16
+ you do buy the commercial licensing option, thank you for your support!
data/README.md CHANGED
@@ -47,13 +47,7 @@ Please see the [sidekiq wiki](https://github.com/mperham/sidekiq/wiki) for more
47
47
  License
48
48
  -----------------
49
49
 
50
- sidekiq is GPLv3 licensed for **non-commercial use only**. For a commercial
51
- license, you must give $50 to my [Pledgie campaign](http://www.pledgie.com/campaigns/16623).
52
- Considering the hundreds of hours I've spent writing OSS, I hope you
53
- think this is a reasonable price. BTW, the commercial license is in
54
- COMM-LICENSE and is the [Sencha commercial license v1.10](http://www.sencha.com/legal/sencha-commercial-software-license-agreement/) with the Support (section 11) terms removed.
55
- Support is provided through GitHub Issues. You are welcome to try the
56
- software for 2 weeks.
50
+ Please see LICENSE for licensing details.
57
51
 
58
52
  <a href='http://www.pledgie.com/campaigns/16623'><img alt='Click here to lend your support to Open Source and make a donation at www.pledgie.com !' src='http://www.pledgie.com/campaigns/16623.png?skin_name=chrome' border='0' /></a>
59
53
 
data/TODO.md CHANGED
@@ -1,4 +1,3 @@
1
- - monitoring stats
2
1
  - resque-ui-esque web ui
3
2
  - graceful shutdown (ideally Celluloid will provide this)
4
3
  - monit/god/etc example scripts
@@ -11,9 +11,11 @@ end
11
11
 
12
12
  begin
13
13
  cli = Sidekiq::CLI.new
14
+ cli.parse
14
15
  cli.run
15
16
  rescue => e
16
17
  raise e if $DEBUG
17
18
  STDERR.puts e.message
19
+ STDERR.puts e.backtrace.join("\n")
18
20
  exit 1
19
21
  end
@@ -0,0 +1,17 @@
1
+ require 'sidekiq'
2
+
3
+ # Start up sidekiq via
4
+ # ./bin/sidekiq -r ./examples/por.rb
5
+ # and then you can open up an IRB session like so:
6
+ # irb -r ./examples/por.rb
7
+ # where you can then say
8
+ # PlainOldRuby.perform_async "like a dog", 3
9
+ #
10
+ class PlainOldRuby
11
+ include Sidekiq::Worker
12
+
13
+ def perform(how_hard="super hard", how_long=1)
14
+ sleep how_long
15
+ puts "Workin' #{how_hard}"
16
+ end
17
+ end
@@ -0,0 +1,57 @@
1
+ # Make sure you have Sinatra installed, then start sidekiq with
2
+ # ./bin/sidekiq -r ./examples/sinkiq.rb
3
+ # Simply run Sinatra with
4
+ # ruby examples/sinkiq.rb
5
+ # and then browse to http://localhost:4567
6
+ #
7
+ require 'sinatra'
8
+ require 'sidekiq/worker'
9
+ $redis = Sidekiq::RedisConnection.create
10
+
11
+ class SinatraWorker
12
+ include Sidekiq::Worker
13
+
14
+ def perform(msg="lulz you forgot a msg!")
15
+ $redis.lpush("sinkiq-example-messages", msg)
16
+ end
17
+ end
18
+
19
+ get '/' do
20
+ @failed = $redis.get('stat:failed')
21
+ @processed = $redis.get('stat:processed')
22
+ @messages = $redis.lrange('sinkiq-example-messages', 0, -1)
23
+ erb :index
24
+ end
25
+
26
+ post '/msg' do
27
+ SinatraWorker.perform_async params[:msg]
28
+ redirect to('/')
29
+ end
30
+
31
+ __END__
32
+
33
+ @@ layout
34
+ <html>
35
+ <head>
36
+ <title>Sinatra + Sidekiq</title>
37
+ <body>
38
+ <%= yield %>
39
+ </body>
40
+ </html>
41
+
42
+ @@ index
43
+ <h1>Sinata + Sidekiq Example</h1>
44
+ <h2>Failed: <%= @failed %></h2>
45
+ <h2>Processed: <%= @processed %></h2>
46
+
47
+ <form method="post" action="/msg">
48
+ <input type="text" name="msg">
49
+ <input type="submit" value="Add Message">
50
+ </form>
51
+
52
+ <a href="/">Refresh page</a>
53
+
54
+ <h3>Messages</h3>
55
+ <% @messages.each do |msg| %>
56
+ <p><%= msg %></p>
57
+ <% end %>
@@ -1,4 +1,4 @@
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' if defined?(::Rails)
@@ -1,34 +1,50 @@
1
+ trap 'INT' do
2
+ # Handle Ctrl-C in JRuby like MRI
3
+ # http://jira.codehaus.org/browse/JRUBY-4637
4
+ Thread.main.raise Interrupt
5
+ end
6
+
7
+ trap 'TERM' do
8
+ # Heroku sends TERM and then waits 10 seconds for process to exit.
9
+ Thread.main.raise Interrupt
10
+ end
11
+
1
12
  require 'optparse'
2
13
  require 'sidekiq/version'
3
14
  require 'sidekiq/util'
4
- require 'sidekiq/client'
15
+ require 'sidekiq/redis_connection'
5
16
  require 'sidekiq/manager'
6
- require 'connection_pool'
7
17
 
8
18
  module Sidekiq
9
19
  class CLI
10
20
  include Util
11
21
 
22
+ # Used for CLI testing
23
+ attr_accessor :options, :code
24
+
12
25
  def initialize
13
- parse_options
14
- validate!
15
- boot_rails
26
+ @code = nil
16
27
  end
17
28
 
18
- FOREVER = 2_000_000_000
29
+ def parse(args=ARGV)
30
+ Sidekiq::Util.logger
31
+ parse_options(args)
32
+ validate!
33
+ boot_system
34
+ end
19
35
 
20
36
  def run
21
- ::Sidekiq::Client.redis = ConnectionPool.new { Redis.connect(:url => @options[:server]) }
22
- manager = Sidekiq::Manager.new(@options[:server], @options)
37
+ Sidekiq::Manager.redis = RedisConnection.create(:url => @options[:server], :namespace => @options[:namespace])
38
+ manager = Sidekiq::Manager.new(@options)
23
39
  begin
24
- log 'Starting processing, hit Ctrl-C to stop'
40
+ logger.info 'Starting processing, hit Ctrl-C to stop'
25
41
  manager.start!
26
42
  # HACK need to determine how to pause main thread while
27
43
  # waiting for signals.
28
- sleep FOREVER
44
+ sleep
29
45
  rescue Interrupt
30
46
  # TODO Need clean shutdown support from Celluloid
31
- log 'Shutting down, pausing 5 seconds to let workers finish...'
47
+ logger.info 'Shutting down, pausing 5 seconds to let workers finish...'
32
48
  manager.stop!
33
49
  manager.wait(:shutdown)
34
50
  end
@@ -36,54 +52,76 @@ module Sidekiq
36
52
 
37
53
  private
38
54
 
39
- def boot_rails
40
- ENV['RAILS_ENV'] = @options[:environment]
41
- require File.expand_path("#{@options[:rails]}/config/environment.rb")
42
- ::Rails.application.eager_load!
55
+ def die(code)
56
+ exit(code)
43
57
  end
44
58
 
45
- def validate!
46
- $DEBUG = @options[:verbose]
59
+ def detected_environment
60
+ @options[:environment] || ENV['RAILS_ENV'] || ENV['RACK_ENV'] || 'development'
61
+ end
62
+
63
+ def boot_system
64
+ ENV['RACK_ENV'] = ENV['RAILS_ENV'] = detected_environment
65
+
66
+ raise ArgumentError, "#{@options[:require]} does not exist" if !File.exist?(@options[:require])
47
67
 
48
- if !File.exist?("#{@options[:rails]}/config/boot.rb")
49
- log "========== Please point sidekiq to a Rails 3 application =========="
50
- log @parser
51
- exit(1)
68
+ if File.directory?(@options[:require])
69
+ require File.expand_path("#{@options[:require]}/config/environment.rb")
70
+ ::Rails.application.eager_load!
71
+ else
72
+ require @options[:require]
52
73
  end
53
74
  end
54
75
 
55
- def parse_options(argv=ARGV)
76
+ def validate!
77
+ @options[:queues] << 'default' if @options[:queues].empty?
78
+ @options[:queues].shuffle!
79
+
80
+ if !File.exist?(@options[:require]) ||
81
+ (File.directory?(@options[:require]) && !File.exist?("#{@options[:require]}/config/application.rb"))
82
+ logger.info "=================================================================="
83
+ logger.info " Please point sidekiq to a Rails 3 application or a Ruby file "
84
+ logger.info " to load your worker classes with -r [DIR|FILE]."
85
+ logger.info "=================================================================="
86
+ logger.info @parser
87
+ die(1)
88
+ end
89
+ end
90
+
91
+ def parse_options(argv)
56
92
  @options = {
57
- :verbose => false,
58
- :queues => ['default'],
93
+ :queues => [],
59
94
  :processor_count => 25,
60
- :server => ENV['REDISTOGO_URL'] || 'redis://localhost:6379/0',
61
- :rails => '.',
62
- :environment => 'production',
95
+ :require => '.',
96
+ :environment => nil,
63
97
  }
64
98
 
65
99
  @parser = OptionParser.new do |o|
66
100
  o.on "-q", "--queue QUEUE,WEIGHT", "Queue to process, with optional weight" do |arg|
67
101
  (q, weight) = arg.split(",")
68
- (weight || 1).times do
102
+ (weight || 1).to_i.times do
69
103
  @options[:queues] << q
70
104
  end
71
105
  end
72
106
 
73
107
  o.on "-v", "--verbose", "Print more verbose output" do
74
- @options[:verbose] = true
108
+ Sidekiq::Util.logger.level = Logger::DEBUG
109
+ end
110
+
111
+ o.on "-n", "--namespace NAMESPACE", "namespace worker queues are under" do |arg|
112
+ @options[:namespace] = arg
75
113
  end
76
114
 
77
115
  o.on "-s", "--server LOCATION", "Where to find Redis" do |arg|
78
116
  @options[:server] = arg
79
117
  end
80
118
 
81
- o.on '-e', '--environment ENV', "Rails application environment" do |arg|
119
+ o.on '-e', '--environment ENV', "Application environment" do |arg|
82
120
  @options[:environment] = arg
83
121
  end
84
122
 
85
- o.on '-r', '--rails PATH', "Location of Rails application with workers" do |arg|
86
- @options[:rails] = arg
123
+ o.on '-r', '--require [PATH|DIR]', "Location of Rails application with workers or file to require" do |arg|
124
+ @options[:require] = arg
87
125
  end
88
126
 
89
127
  o.on '-c', '--concurrency INT', "processor threads to use" do |arg|
@@ -93,8 +131,8 @@ module Sidekiq
93
131
 
94
132
  @parser.banner = "sidekiq [options]"
95
133
  @parser.on_tail "-h", "--help", "Show help" do
96
- log @parser
97
- exit 1
134
+ logger.info @parser
135
+ die 1
98
136
  end
99
137
  @parser.parse!(argv)
100
138
  end
@@ -1,30 +1,53 @@
1
1
  require 'multi_json'
2
2
  require 'redis'
3
3
 
4
+ require 'sidekiq/redis_connection'
5
+ require 'sidekiq/middleware/chain'
6
+ require 'sidekiq/middleware/client/resque_web_compatibility'
7
+ require 'sidekiq/middleware/client/unique_jobs'
8
+
4
9
  module Sidekiq
5
10
  class Client
6
11
 
7
- def self.redis
8
- @redis ||= begin
9
- # autoconfig for Heroku
10
- hash = {}
11
- hash[:url] = ENV['REDISTOGO_URL'] if ENV['REDISTOGO_URL']
12
- Redis.connect(hash)
12
+ def self.middleware
13
+ @middleware ||= begin
14
+ m = Middleware::Chain.new
15
+ m.register do
16
+ use Middleware::Client::UniqueJobs, Client.redis
17
+ use Middleware::Client::ResqueWebCompatibility, Client.redis
18
+ end
19
+ m
13
20
  end
14
21
  end
15
22
 
23
+ def self.queues
24
+ @queues ||= {}
25
+ end
26
+
27
+ def self.redis
28
+ @redis ||= RedisConnection.create
29
+ end
30
+
16
31
  def self.redis=(redis)
17
32
  @redis = redis
18
33
  end
19
34
 
20
35
  # Example usage:
21
36
  # Sidekiq::Client.push('my_queue', 'class' => MyWorker, 'args' => ['foo', 1, :bat => 'bar'])
22
- def self.push(queue='default', item)
37
+ def self.push(queue=nil, item)
23
38
  raise(ArgumentError, "Message must be a Hash of the form: { 'class' => SomeClass, 'args' => ['bob', 1, :foo => 'bar'] }") unless item.is_a?(Hash)
24
39
  raise(ArgumentError, "Message must include a class and set of arguments: #{item.inspect}") if !item['class'] || !item['args']
25
40
 
41
+ queue = queue || queues[item['class'].to_s] || 'default'
42
+
26
43
  item['class'] = item['class'].to_s if !item['class'].is_a?(String)
27
- redis.rpush("queue:#{queue}", MultiJson.encode(item))
44
+
45
+ pushed = false
46
+ middleware.invoke(item, queue) do
47
+ redis.rpush("queue:#{queue}", MultiJson.encode(item))
48
+ pushed = true
49
+ end
50
+ pushed
28
51
  end
29
52
 
30
53
  # Please use .push if possible instead.
@@ -33,16 +56,10 @@ module Sidekiq
33
56
  #
34
57
  # Sidekiq::Client.enqueue(MyWorker, 'foo', 1, :bat => 'bar')
35
58
  #
36
- # Messages are enqueued to the 'default' queue. Optionally,
37
- # MyWorker can define a queue class method:
38
- #
39
- # def self.queue
40
- # 'my_queue'
41
- # end
59
+ # Messages are enqueued to the 'default' queue.
42
60
  #
43
61
  def self.enqueue(klass, *args)
44
- queue = (klass.respond_to?(:queue) && klass.queue) || 'default'
45
- push(queue, { 'class' => klass.name, 'args' => args })
62
+ push(nil, { 'class' => klass.name, 'args' => args })
46
63
  end
47
64
  end
48
65
  end
@@ -18,22 +18,20 @@ module Sidekiq
18
18
 
19
19
  trap_exit :processor_died
20
20
 
21
- def initialize(location, options={})
22
- log "Booting sidekiq #{Sidekiq::VERSION} with Redis at #{location}"
23
- verbose options.inspect
21
+ class << self
22
+ attr_accessor :redis
23
+ end
24
+
25
+ def initialize(options={})
26
+ logger.info "Booting sidekiq #{Sidekiq::VERSION} with Redis at #{redis.client.location}"
27
+ logger.debug { options.inspect }
24
28
  @count = options[:processor_count] || 25
25
29
  @queues = options[:queues]
26
- @queue_idx = 0
27
- @queues_size = @queues.size
28
- @redis = Redis.connect(:url => location)
29
30
  @done_callback = nil
30
31
 
31
32
  @done = false
32
33
  @busy = []
33
- @ready = []
34
- @count.times do
35
- @ready << Processor.new_link(current_actor)
36
- end
34
+ @ready = @count.times.map { Processor.new_link(current_actor) }
37
35
  end
38
36
 
39
37
  def stop
@@ -44,35 +42,38 @@ module Sidekiq
44
42
  after(5) do
45
43
  signal(:shutdown)
46
44
  end
45
+
46
+ redis.with_connection do |conn|
47
+ conn.smembers('workers').each do |name|
48
+ conn.srem('workers', name) if name =~ /:#{Process.pid}:/
49
+ end
50
+ end
47
51
  end
48
52
 
49
53
  def start
50
54
  dispatch(true)
51
55
  end
52
56
 
53
- def when_done
54
- @done_callback = Proc.new
57
+ def when_done(&blk)
58
+ @done_callback = blk
55
59
  end
56
60
 
57
61
  def processor_done(processor)
58
- @done_callback.call(processor) if @done_callback
59
- @busy.delete(processor)
60
- if stopped?
61
- processor.terminate
62
- else
63
- @ready << processor
62
+ watchdog('sidekiq processor_done crashed!') do
63
+ @done_callback.call(processor) if @done_callback
64
+ @busy.delete(processor)
65
+ if stopped?
66
+ processor.terminate
67
+ else
68
+ @ready << processor
69
+ end
70
+ dispatch
64
71
  end
65
- dispatch
66
72
  end
67
73
 
68
74
  def processor_died(processor, reason)
69
75
  @busy.delete(processor)
70
76
 
71
- if reason
72
- log "Processor death: #{reason}"
73
- log reason.backtrace.join("\n")
74
- end
75
-
76
77
  unless stopped?
77
78
  @ready << Processor.new_link(current_actor)
78
79
  dispatch
@@ -81,13 +82,12 @@ module Sidekiq
81
82
 
82
83
  private
83
84
 
84
- def find_work(queue_idx)
85
- current_queue = @queues[queue_idx]
86
- msg = @redis.lpop("queue:#{current_queue}")
85
+ def find_work(queue)
86
+ msg = redis.lpop("queue:#{queue}")
87
87
  if msg
88
88
  processor = @ready.pop
89
89
  @busy << processor
90
- processor.process! MultiJson.decode(msg)
90
+ processor.process!(MultiJson.decode(msg), queue)
91
91
  end
92
92
  !!msg
93
93
  end
@@ -96,32 +96,22 @@ module Sidekiq
96
96
  watchdog("Fatal error in sidekiq, dispatch loop died") do
97
97
  return if stopped?
98
98
 
99
- # Our dispatch loop
100
- # Loop through the queues, looking for a message in each.
101
- queue_idx = 0
102
- found = false
99
+ # Dispatch loop
103
100
  loop do
104
- # return so that we don't dispatch again until processor_done
105
- break verbose('no processors') if @ready.size == 0
106
-
107
- found ||= find_work(queue_idx)
108
- queue_idx += 1
109
-
110
- # if we find no messages in any of the queues, we can break
111
- # out of the loop. Otherwise we loop again.
112
- lastq = (queue_idx % @queues.size == 0)
113
- if lastq && !found
114
- verbose('nothing to process'); break
115
- elsif lastq
116
- queue_idx = 0
117
- found = false
101
+ break logger.debug('no processors') if @ready.empty?
102
+ found = false
103
+ @ready.size.times do
104
+ found ||= find_work(@queues.sample)
118
105
  end
106
+ break logger.debug('nothing to process') unless found
119
107
  end
120
108
 
121
109
  # This is the polling loop that ensures we check Redis every
122
110
  # second for work, even if there was nothing to do this time
123
111
  # around.
124
- after(1) { verbose('ping'); dispatch(schedule) } if schedule
112
+ after(1) do
113
+ dispatch(schedule)
114
+ end if schedule
125
115
  end
126
116
  end
127
117