sidekiq 0.6.0 → 0.7.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 (45) hide show
  1. data/.travis.yml +3 -0
  2. data/Changes.md +15 -0
  3. data/README.md +1 -1
  4. data/TODO.md +0 -2
  5. data/examples/chef/cookbooks/sidekiq/README.rdoc +7 -0
  6. data/examples/chef/cookbooks/sidekiq/recipes/default.rb +54 -0
  7. data/examples/chef/cookbooks/sidekiq/templates/default/monitrc.conf.erb +8 -0
  8. data/examples/chef/cookbooks/sidekiq/templates/default/sidekiq.erb +219 -0
  9. data/examples/chef/cookbooks/sidekiq/templates/default/sidekiq.yml.erb +28 -0
  10. data/examples/monitrc.conf +6 -0
  11. data/examples/por.rb +10 -0
  12. data/lib/sidekiq.rb +70 -0
  13. data/lib/sidekiq/cli.rb +72 -30
  14. data/lib/sidekiq/client.rb +16 -16
  15. data/lib/sidekiq/extensions/action_mailer.rb +27 -0
  16. data/lib/sidekiq/extensions/active_record.rb +29 -0
  17. data/lib/sidekiq/extensions/generic_proxy.rb +21 -0
  18. data/lib/sidekiq/manager.rb +16 -12
  19. data/lib/sidekiq/middleware/chain.rb +23 -23
  20. data/lib/sidekiq/middleware/client/resque_web_compatibility.rb +1 -4
  21. data/lib/sidekiq/middleware/client/unique_jobs.rb +5 -13
  22. data/lib/sidekiq/middleware/server/failure_jobs.rb +24 -0
  23. data/lib/sidekiq/middleware/server/unique_jobs.rb +1 -5
  24. data/lib/sidekiq/processor.rb +11 -13
  25. data/lib/sidekiq/redis_connection.rb +1 -1
  26. data/lib/sidekiq/util.rb +20 -1
  27. data/lib/sidekiq/version.rb +1 -1
  28. data/lib/sidekiq/worker.rb +1 -1
  29. data/myapp/Gemfile +0 -2
  30. data/myapp/app/controllers/work_controller.rb +25 -1
  31. data/myapp/app/mailers/user_mailer.rb +9 -0
  32. data/myapp/app/models/post.rb +3 -0
  33. data/myapp/app/views/user_mailer/greetings.html.erb +3 -0
  34. data/myapp/app/workers/hard_worker.rb +2 -2
  35. data/myapp/config/initializers/sidekiq.rb +6 -1
  36. data/myapp/config/routes.rb +3 -0
  37. data/sidekiq.gemspec +2 -0
  38. data/test/config.yml +11 -0
  39. data/test/test_cli.rb +119 -3
  40. data/test/test_client.rb +21 -10
  41. data/test/test_extensions.rb +45 -0
  42. data/test/test_manager.rb +2 -2
  43. data/test/test_middleware.rb +7 -12
  44. data/test/test_stats.rb +1 -2
  45. metadata +55 -16
data/lib/sidekiq/cli.rb CHANGED
@@ -9,8 +9,9 @@ trap 'TERM' do
9
9
  Thread.main.raise Interrupt
10
10
  end
11
11
 
12
+ require 'yaml'
12
13
  require 'optparse'
13
- require 'sidekiq/version'
14
+ require 'sidekiq'
14
15
  require 'sidekiq/util'
15
16
  require 'sidekiq/redis_connection'
16
17
  require 'sidekiq/manager'
@@ -20,7 +21,7 @@ module Sidekiq
20
21
  include Util
21
22
 
22
23
  # Used for CLI testing
23
- attr_accessor :options, :code
24
+ attr_accessor :code
24
25
 
25
26
  def initialize
26
27
  @code = nil
@@ -28,14 +29,21 @@ module Sidekiq
28
29
 
29
30
  def parse(args=ARGV)
30
31
  Sidekiq::Util.logger
31
- parse_options(args)
32
+
33
+ cli = parse_options(args)
34
+ config = parse_config(cli)
35
+ options.merge!(config.merge(cli))
36
+
37
+ set_logger_level_to_debug if options[:verbose]
38
+
39
+ write_pid
32
40
  validate!
33
41
  boot_system
34
42
  end
35
43
 
36
44
  def run
37
- Sidekiq::Manager.redis = RedisConnection.create(:url => @options[:server], :namespace => @options[:namespace])
38
- manager = Sidekiq::Manager.new(@options)
45
+ Sidekiq.redis ||= RedisConnection.create(:url => options[:server], :namespace => options[:namespace])
46
+ manager = Sidekiq::Manager.new(options)
39
47
  begin
40
48
  logger.info 'Starting processing, hit Ctrl-C to stop'
41
49
  manager.start!
@@ -44,7 +52,7 @@ module Sidekiq
44
52
  sleep
45
53
  rescue Interrupt
46
54
  # TODO Need clean shutdown support from Celluloid
47
- logger.info 'Shutting down, pausing 5 seconds to let workers finish...'
55
+ logger.info 'Shutting down'
48
56
  manager.stop!
49
57
  manager.wait(:shutdown)
50
58
  end
@@ -56,29 +64,33 @@ module Sidekiq
56
64
  exit(code)
57
65
  end
58
66
 
67
+ def options
68
+ Sidekiq.options
69
+ end
70
+
59
71
  def detected_environment
60
- @options[:environment] || ENV['RAILS_ENV'] || ENV['RACK_ENV'] || 'development'
72
+ options[:environment] ||= ENV['RAILS_ENV'] || ENV['RACK_ENV'] || 'development'
61
73
  end
62
74
 
63
75
  def boot_system
64
76
  ENV['RACK_ENV'] = ENV['RAILS_ENV'] = detected_environment
65
77
 
66
- raise ArgumentError, "#{@options[:require]} does not exist" if !File.exist?(@options[:require])
78
+ raise ArgumentError, "#{options[:require]} does not exist" unless File.exist?(options[:require])
67
79
 
68
- if File.directory?(@options[:require])
69
- require File.expand_path("#{@options[:require]}/config/environment.rb")
80
+ if File.directory?(options[:require])
81
+ require File.expand_path("#{options[:require]}/config/environment.rb")
70
82
  ::Rails.application.eager_load!
71
83
  else
72
- require @options[:require]
84
+ require options[:require]
73
85
  end
74
86
  end
75
87
 
76
88
  def validate!
77
- @options[:queues] << 'default' if @options[:queues].empty?
78
- @options[:queues].shuffle!
89
+ options[:queues] << 'default' if options[:queues].empty?
90
+ options[:queues].shuffle!
79
91
 
80
- if !File.exist?(@options[:require]) ||
81
- (File.directory?(@options[:require]) && !File.exist?("#{@options[:require]}/config/application.rb"))
92
+ if !File.exist?(options[:require]) ||
93
+ (File.directory?(options[:require]) && !File.exist?("#{options[:require]}/config/application.rb"))
82
94
  logger.info "=================================================================="
83
95
  logger.info " Please point sidekiq to a Rails 3 application or a Ruby file "
84
96
  logger.info " to load your worker classes with -r [DIR|FILE]."
@@ -89,43 +101,44 @@ module Sidekiq
89
101
  end
90
102
 
91
103
  def parse_options(argv)
92
- @options = {
93
- :queues => [],
94
- :processor_count => 25,
95
- :require => '.',
96
- :environment => nil,
97
- }
104
+ opts = {}
98
105
 
99
106
  @parser = OptionParser.new do |o|
100
107
  o.on "-q", "--queue QUEUE,WEIGHT", "Queue to process, with optional weight" do |arg|
101
108
  (q, weight) = arg.split(",")
102
- (weight || 1).to_i.times do
103
- @options[:queues] << q
104
- end
109
+ parse_queues(opts, q, weight)
105
110
  end
106
111
 
107
112
  o.on "-v", "--verbose", "Print more verbose output" do
108
- Sidekiq::Util.logger.level = Logger::DEBUG
113
+ set_logger_level_to_debug
109
114
  end
110
115
 
111
116
  o.on "-n", "--namespace NAMESPACE", "namespace worker queues are under" do |arg|
112
- @options[:namespace] = arg
117
+ opts[:namespace] = arg
113
118
  end
114
119
 
115
120
  o.on "-s", "--server LOCATION", "Where to find Redis" do |arg|
116
- @options[:server] = arg
121
+ opts[:server] = arg
117
122
  end
118
123
 
119
124
  o.on '-e', '--environment ENV', "Application environment" do |arg|
120
- @options[:environment] = arg
125
+ opts[:environment] = arg
121
126
  end
122
127
 
123
128
  o.on '-r', '--require [PATH|DIR]', "Location of Rails application with workers or file to require" do |arg|
124
- @options[:require] = arg
129
+ opts[:require] = arg
125
130
  end
126
131
 
127
132
  o.on '-c', '--concurrency INT', "processor threads to use" do |arg|
128
- @options[:processor_count] = arg.to_i
133
+ opts[:concurrency] = arg.to_i
134
+ end
135
+
136
+ o.on '-P', '--pidfile PATH', "path to pidfile" do |arg|
137
+ opts[:pidfile] = arg
138
+ end
139
+
140
+ o.on '-C', '--config PATH', "path to YAML config file" do |arg|
141
+ opts[:config_file] = arg
129
142
  end
130
143
  end
131
144
 
@@ -135,6 +148,35 @@ module Sidekiq
135
148
  die 1
136
149
  end
137
150
  @parser.parse!(argv)
151
+ opts
152
+ end
153
+
154
+ def write_pid
155
+ if path = options[:pidfile]
156
+ File.open(path, 'w') do |f|
157
+ f.puts Process.pid
158
+ end
159
+ end
160
+ end
161
+
162
+ def parse_config(cli)
163
+ opts = {}
164
+ if cli[:config_file] && File.exist?(cli[:config_file])
165
+ opts = YAML.load_file cli[:config_file]
166
+ queues = opts.delete(:queues) || []
167
+ queues.each { |pair| parse_queues(opts, *pair) }
168
+ end
169
+ opts
170
+ end
171
+
172
+ def parse_queues(opts, q, weight)
173
+ (weight || 1).to_i.times do
174
+ (opts[:queues] ||= []) << q
175
+ end
176
+ end
177
+
178
+ def set_logger_level_to_debug
179
+ Sidekiq::Util.logger.level = Logger::DEBUG
138
180
  end
139
181
 
140
182
  end
@@ -10,26 +10,26 @@ module Sidekiq
10
10
  class Client
11
11
 
12
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
+ raise "Sidekiq::Client.middleware is now Sidekiq.client_middleware"
14
+ end
15
+
16
+ def self.default_middleware
17
+ Middleware::Chain.new do |m|
18
+ m.add Middleware::Client::UniqueJobs
19
+ m.add Middleware::Client::ResqueWebCompatibility
20
20
  end
21
21
  end
22
22
 
23
- def self.queues
24
- @queues ||= {}
23
+ def self.registered_workers
24
+ Sidekiq.redis.smembers('workers')
25
25
  end
26
26
 
27
- def self.redis
28
- @redis ||= RedisConnection.create
27
+ def self.registered_queues
28
+ Sidekiq.redis.smembers('queues')
29
29
  end
30
30
 
31
- def self.redis=(redis)
32
- @redis = redis
31
+ def self.queue_mappings
32
+ @queue_mappings ||= {}
33
33
  end
34
34
 
35
35
  # Example usage:
@@ -38,13 +38,13 @@ module Sidekiq
38
38
  raise(ArgumentError, "Message must be a Hash of the form: { 'class' => SomeClass, 'args' => ['bob', 1, :foo => 'bar'] }") unless item.is_a?(Hash)
39
39
  raise(ArgumentError, "Message must include a class and set of arguments: #{item.inspect}") if !item['class'] || !item['args']
40
40
 
41
- queue = queue || queues[item['class'].to_s] || 'default'
41
+ queue = queue || queue_mappings[item['class'].to_s] || 'default'
42
42
 
43
43
  item['class'] = item['class'].to_s if !item['class'].is_a?(String)
44
44
 
45
45
  pushed = false
46
- middleware.invoke(item, queue) do
47
- redis.rpush("queue:#{queue}", MultiJson.encode(item))
46
+ Sidekiq.client_middleware.invoke(item, queue) do
47
+ Sidekiq.redis.rpush("queue:#{queue}", MultiJson.encode(item))
48
48
  pushed = true
49
49
  end
50
50
  pushed
@@ -0,0 +1,27 @@
1
+ require 'sidekiq/extensions/generic_proxy'
2
+
3
+ module Sidekiq
4
+ module Extensions
5
+ ##
6
+ # Adds a 'delay' method to ActionMailer to offload arbitrary email
7
+ # delivery to Sidekiq. Example:
8
+ #
9
+ # UserMailer.delay.send_welcome_email(new_user)
10
+ class DelayedMailer
11
+ include Sidekiq::Worker
12
+
13
+ def perform(yml)
14
+ (target, method_name, args) = YAML.load(yml)
15
+ target.send(method_name, *args).deliver
16
+ end
17
+ end
18
+
19
+ module ActionMailer
20
+ def delay
21
+ Proxy.new(DelayedMailer, self)
22
+ end
23
+ end
24
+
25
+ ::ActionMailer::Base.extend(ActionMailer)
26
+ end
27
+ end if defined?(::ActionMailer)
@@ -0,0 +1,29 @@
1
+ require 'sidekiq/extensions/generic_proxy'
2
+
3
+ module Sidekiq
4
+ module Extensions
5
+ ##
6
+ # Adds a 'delay' method to ActiveRecord to offload arbitrary method
7
+ # execution to Sidekiq. Examples:
8
+ #
9
+ # User.delay.delete_inactive
10
+ # User.recent_signups.each { |user| user.delay.mark_as_awesome }
11
+ class DelayedModel
12
+ include Sidekiq::Worker
13
+
14
+ def perform(yml)
15
+ (target, method_name, args) = YAML.load(yml)
16
+ target.send(method_name, *args)
17
+ end
18
+ end
19
+
20
+ module ActiveRecord
21
+ def delay
22
+ Proxy.new(DelayedModel, self)
23
+ end
24
+ end
25
+
26
+ ::ActiveRecord::Base.extend(ActiveRecord)
27
+ ::ActiveRecord::Base.send(:include, ActiveRecord)
28
+ end
29
+ end if defined?(::ActiveRecord)
@@ -0,0 +1,21 @@
1
+ module Sidekiq
2
+ module Extensions
3
+ class Proxy < ::BasicObject
4
+ def initialize(performable, target)
5
+ @performable = performable
6
+ @target = target
7
+ end
8
+
9
+ def method_missing(name, *args)
10
+ # Sidekiq has a limitation in that its message must be JSON.
11
+ # JSON can't round trip real Ruby objects so we use YAML to
12
+ # serialize the objects to a String. The YAML will be converted
13
+ # to JSON and then deserialized on the other side back into a
14
+ # Ruby object.
15
+ obj = [@target, name, args]
16
+ ::Sidekiq::Client.push('class' => @performable.name, 'args' => [::YAML.dump(obj)])
17
+ end
18
+ end
19
+
20
+ end
21
+ end
@@ -18,14 +18,10 @@ module Sidekiq
18
18
 
19
19
  trap_exit :processor_died
20
20
 
21
- class << self
22
- attr_accessor :redis
23
- end
24
-
25
21
  def initialize(options={})
26
22
  logger.info "Booting sidekiq #{Sidekiq::VERSION} with Redis at #{redis.client.location}"
27
23
  logger.debug { options.inspect }
28
- @count = options[:processor_count] || 25
24
+ @count = options[:concurrency] || 25
29
25
  @queues = options[:queues]
30
26
  @done_callback = nil
31
27
 
@@ -39,15 +35,23 @@ module Sidekiq
39
35
  @ready.each(&:terminate)
40
36
  @ready.clear
41
37
 
42
- after(5) do
43
- signal(:shutdown)
44
- end
45
-
46
38
  redis.with_connection do |conn|
47
- conn.smembers('workers').each do |name|
48
- conn.srem('workers', name) if name =~ /:#{Process.pid}:/
39
+ workers = conn.smembers('workers')
40
+ workers.each do |name|
41
+ conn.srem('workers', name) if name =~ /:#{process_id}-/
49
42
  end
50
43
  end
44
+
45
+ if @busy.empty?
46
+ return signal(:shutdown)
47
+ end
48
+
49
+ logger.info("Pausing 5 seconds to allow workers to finish...")
50
+ after(5) do
51
+ @busy.each(&:terminate)
52
+ #@busy.each(&:requeue)
53
+ signal(:shutdown)
54
+ end
51
55
  end
52
56
 
53
57
  def start
@@ -63,7 +67,7 @@ module Sidekiq
63
67
  @done_callback.call(processor) if @done_callback
64
68
  @busy.delete(processor)
65
69
  if stopped?
66
- processor.terminate
70
+ processor.terminate if processor.alive?
67
71
  else
68
72
  @ready << processor
69
73
  end
@@ -5,34 +5,37 @@ module Sidekiq
5
5
  # (pushing jobs onto the queue) as well as the server
6
6
  # side (when jobs are actually processed).
7
7
  #
8
- # Default middleware for the server side:
8
+ # To add middleware for the client:
9
9
  #
10
- # Sidekiq::Processor.middleware.register do
11
- # use Middleware::Server::Airbrake
12
- # use Middleware::Server::ActiveRecord
10
+ # Sidekiq.client_middleware do |chain|
11
+ # chain.add MyClientHook
13
12
  # end
14
13
  #
15
- # To add middleware for the client, do:
14
+ # To modify middleware for the server, just call
15
+ # with another block:
16
16
  #
17
- # Sidekiq::Client.middleware.register do
18
- # use MyClientHook
17
+ # Sidekiq.server_middleware do |chain|
18
+ # chain.add MyServerHook
19
+ # chain.remove ActiveRecord
19
20
  # end
20
21
  #
21
- # To add middleware for the server, do:
22
+ # This is an example of a minimal server middleware:
22
23
  #
23
- # Sidekiq::Processor.middleware.register do
24
- # use MyServerHook
24
+ # class MyServerHook
25
+ # def call(worker, msg, queue)
26
+ # puts "Before work"
27
+ # yield
28
+ # puts "After work"
29
+ # end
25
30
  # end
26
31
  #
27
- # This is an example of a minimal middleware:
32
+ # This is an example of a minimal client middleware:
28
33
  #
29
- # class MyHook
30
- # def initialize(options=nil)
31
- # end
32
- # def call(worker, msg)
33
- # puts "Before work"
34
+ # class MyClientHook
35
+ # def call(msg, queue)
36
+ # puts "Before push"
34
37
  # yield
35
- # puts "After work"
38
+ # puts "After push"
36
39
  # end
37
40
  # end
38
41
  #
@@ -42,17 +45,14 @@ module Sidekiq
42
45
 
43
46
  def initialize
44
47
  @entries = []
48
+ yield self if block_given?
45
49
  end
46
50
 
47
- def register(&block)
48
- instance_eval(&block)
49
- end
50
-
51
- def unregister(klass)
51
+ def remove(klass)
52
52
  entries.delete_if { |entry| entry.klass == klass }
53
53
  end
54
54
 
55
- def use(klass, *args)
55
+ def add(klass, *args)
56
56
  entries << Entry.new(klass, *args) unless exists?(klass)
57
57
  end
58
58