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,92 @@
1
+ module Sidekiq
2
+ # Middleware is code configured to run before/after
3
+ # a message is processed. It is patterned after Rack
4
+ # middleware. Middleware exists for the client side
5
+ # (pushing jobs onto the queue) as well as the server
6
+ # side (when jobs are actually processed).
7
+ #
8
+ # Default middleware for the server side:
9
+ #
10
+ # Sidekiq::Processor.middleware.register do
11
+ # use Middleware::Server::Airbrake
12
+ # use Middleware::Server::ActiveRecord
13
+ # end
14
+ #
15
+ # To add middleware for the client, do:
16
+ #
17
+ # Sidekiq::Client.middleware.register do
18
+ # use MyClientHook
19
+ # end
20
+ #
21
+ # To add middleware for the server, do:
22
+ #
23
+ # Sidekiq::Processor.middleware.register do
24
+ # use MyServerHook
25
+ # end
26
+ #
27
+ # This is an example of a minimal middleware:
28
+ #
29
+ # class MyHook
30
+ # def initialize(options=nil)
31
+ # end
32
+ # def call(worker, msg)
33
+ # puts "Before work"
34
+ # yield
35
+ # puts "After work"
36
+ # end
37
+ # end
38
+ #
39
+ module Middleware
40
+ class Chain
41
+ attr_reader :entries
42
+
43
+ def initialize
44
+ @entries = []
45
+ end
46
+
47
+ def register(&block)
48
+ instance_eval(&block)
49
+ end
50
+
51
+ def unregister(klass)
52
+ entries.delete_if { |entry| entry.klass == klass }
53
+ end
54
+
55
+ def use(klass, *args)
56
+ entries << Entry.new(klass, *args) unless exists?(klass)
57
+ end
58
+
59
+ def exists?(klass)
60
+ entries.any? { |entry| entry.klass == klass }
61
+ end
62
+
63
+ def retrieve
64
+ entries.map(&:make_new)
65
+ end
66
+
67
+ def invoke(*args, &final_action)
68
+ chain = retrieve.dup
69
+ traverse_chain = lambda do
70
+ if chain.empty?
71
+ final_action.call
72
+ else
73
+ chain.shift.call(*args, &traverse_chain)
74
+ end
75
+ end
76
+ traverse_chain.call
77
+ end
78
+ end
79
+
80
+ class Entry
81
+ attr_reader :klass
82
+ def initialize(klass, *args)
83
+ @klass = klass
84
+ @args = args
85
+ end
86
+
87
+ def make_new
88
+ @klass.new(*@args)
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,17 @@
1
+ module Sidekiq
2
+ module Middleware
3
+ module Client
4
+ class ResqueWebCompatibility
5
+ def initialize(redis)
6
+ @redis = redis
7
+ end
8
+
9
+ def call(item, queue)
10
+ @redis.sadd('queues', queue)
11
+ yield
12
+ end
13
+
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,30 @@
1
+ require 'digest'
2
+
3
+ module Sidekiq
4
+ module Middleware
5
+ module Client
6
+ class UniqueJobs
7
+ HASH_KEY_EXPIRATION = 30 * 60
8
+
9
+ def initialize(redis)
10
+ @redis = redis
11
+ end
12
+
13
+ def call(item, queue)
14
+ payload_hash = Digest::MD5.hexdigest(MultiJson.encode(item))
15
+ return if already_scheduled?(payload_hash)
16
+
17
+ @redis.setex(payload_hash, HASH_KEY_EXPIRATION, 1)
18
+
19
+ yield
20
+ end
21
+
22
+ private
23
+
24
+ def already_scheduled?(payload_hash)
25
+ !!@redis.get(payload_hash)
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,13 @@
1
+ module Sidekiq
2
+ module Middleware
3
+ module Server
4
+ class ActiveRecord
5
+ def call(*args)
6
+ yield
7
+ ensure
8
+ ::ActiveRecord::Base.clear_active_connections! if defined?(::ActiveRecord)
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,30 @@
1
+ require 'sidekiq/util'
2
+
3
+ module Sidekiq
4
+ module Middleware
5
+ module Server
6
+ class Airbrake
7
+ include Util
8
+ def call(*args)
9
+ yield
10
+ rescue => ex
11
+ logger.warn ex
12
+ logger.warn ex.backtrace.join("\n")
13
+ send_to_airbrake(args[1], ex) if defined?(::Airbrake)
14
+ raise
15
+ end
16
+
17
+ private
18
+
19
+ def send_to_airbrake(msg, ex)
20
+ ::Airbrake.notify(:error_class => ex.class.name,
21
+ :error_message => "#{ex.class.name}: #{ex.message}",
22
+ :parameters => msg)
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+
29
+
30
+
@@ -0,0 +1,17 @@
1
+ module Sidekiq
2
+ module Middleware
3
+ module Server
4
+ class UniqueJobs
5
+ def initialize(redis)
6
+ @redis = redis
7
+ end
8
+
9
+ def call(*args)
10
+ yield
11
+ ensure
12
+ @redis.del(Digest::MD5.hexdigest(MultiJson.encode(args[1])))
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -1,37 +1,95 @@
1
- require 'sidekiq/util'
2
- require 'sidekiq/middleware'
3
1
  require 'celluloid'
4
2
 
3
+ require 'sidekiq/util'
4
+ require 'sidekiq/middleware/chain'
5
+ require 'sidekiq/middleware/server/active_record'
6
+ require 'sidekiq/middleware/server/airbrake'
7
+ require 'sidekiq/middleware/server/unique_jobs'
8
+
5
9
  module Sidekiq
6
10
  class Processor
7
11
  include Util
8
12
  include Celluloid
9
13
 
10
- def initialize(boss)
11
- @boss = boss
14
+ def self.middleware
15
+ @middleware ||= begin
16
+ chain = Middleware::Chain.new
17
+
18
+ # default middleware
19
+ chain.register do
20
+ use Middleware::Server::Airbrake
21
+ use Middleware::Server::UniqueJobs, Sidekiq::Manager.redis
22
+ use Middleware::Server::ActiveRecord
23
+ end
24
+ chain
25
+ end
12
26
  end
13
27
 
14
- def process(msg)
15
- klass = constantize(msg['class'])
16
- invoke_chain(klass.new, msg)
17
- @boss.processor_done!(current_actor)
28
+ def initialize(boss)
29
+ @boss = boss
30
+ redis.sadd('workers', self)
18
31
  end
19
32
 
20
- def invoke_chain(worker, msg)
21
- chain = Sidekiq::Middleware::Chain.retrieve.dup
22
- traverse_chain = lambda do
23
- if chain.empty?
33
+ def process(msg, queue)
34
+ klass = constantize(msg['class'])
35
+ worker = klass.new
36
+ stats(worker, msg, queue) do
37
+ self.class.middleware.invoke(worker, msg, queue) do
24
38
  worker.perform(*msg['args'])
25
- else
26
- chain.shift.call(worker, msg, &traverse_chain)
27
39
  end
28
40
  end
29
- traverse_chain.call
41
+ @boss.processor_done!(current_actor)
30
42
  end
31
43
 
32
44
  # See http://github.com/tarcieri/celluloid/issues/22
33
45
  def inspect
34
- "Sidekiq::Processor<#{object_id}>"
46
+ "#<Processor #{to_s}>"
47
+ end
48
+
49
+ def to_s
50
+ @str ||= "#{hostname}:#{Process.pid}-#{Thread.current.object_id}:default"
51
+ end
52
+
53
+ private
54
+
55
+ def stats(worker, msg, queue)
56
+ redis.with_connection do |conn|
57
+ conn.multi do
58
+ conn.set("worker:#{self}:started", Time.now.to_s)
59
+ conn.set("worker:#{self}", MultiJson.encode(:queue => queue, :payload => msg,
60
+ :run_at => Time.now.strftime("%Y/%m/%d %H:%M:%S %Z")))
61
+ end
62
+ end
63
+
64
+ dying = false
65
+ begin
66
+ yield
67
+ rescue
68
+ dying = true
69
+ # Uh oh, error. We will die so unregister as much as we can first.
70
+ redis.with_connection do |conn|
71
+ conn.multi do
72
+ conn.incrby("stat:failed", 1)
73
+ conn.del("stat:processed:#{self}")
74
+ conn.srem("workers", self)
75
+ end
76
+ end
77
+ raise
78
+ ensure
79
+ redis.with_connection do |conn|
80
+ conn.multi do
81
+ conn.del("worker:#{self}")
82
+ conn.del("worker:#{self}:started")
83
+ conn.incrby("stat:processed", 1)
84
+ conn.incrby("stat:processed:#{self}", 1) unless dying
85
+ end
86
+ end
87
+ end
88
+
89
+ end
90
+
91
+ def hostname
92
+ @h ||= `hostname`.strip
35
93
  end
36
94
  end
37
95
  end
@@ -0,0 +1,23 @@
1
+ require 'connection_pool'
2
+ require 'redis/namespace'
3
+
4
+ module Sidekiq
5
+ class RedisConnection
6
+ def self.create(options={})
7
+ url = options[:url] || ENV['REDISTOGO_URL'] || 'redis://localhost:6379/0'
8
+ client = build_client(url, options[:namespace])
9
+ return ConnectionPool.new(:timeout => 1, :size => 25) { client } unless options[:use_pool] == false
10
+ client
11
+ end
12
+
13
+ def self.build_client(url, namespace)
14
+ client = Redis.connect(:url => url)
15
+ if namespace
16
+ Redis::Namespace.new(namespace, :redis => client)
17
+ else
18
+ client
19
+ end
20
+ end
21
+ private_class_method :build_client
22
+ end
23
+ end
@@ -0,0 +1,34 @@
1
+ module Sidekiq
2
+ module Worker
3
+
4
+ ##
5
+ # The Sidekiq testing infrastructure overrides perform_async
6
+ # so that it does not actually touch the network. Instead it
7
+ # stores the asynchronous jobs in a per-class array so that
8
+ # their presence/absence can be asserted by your tests.
9
+ #
10
+ # This is similar to ActionMailer's :test delivery_method and its
11
+ # ActionMailer::Base.deliveries array.
12
+ #
13
+ # Example:
14
+ #
15
+ # require 'sidekiq/testing'
16
+ #
17
+ # assert_equal 0, HardWorker.jobs.size
18
+ # HardWorker.perform_async(:something)
19
+ # assert_equal 1, HardWorker.jobs.size
20
+ # assert_equal :something, HardWorker.jobs[0]['args'][0]
21
+ #
22
+ module ClassMethods
23
+ alias_method :perform_async_old, :perform_async
24
+ def perform_async(*args)
25
+ jobs << { 'class' => self.name, 'args' => args }
26
+ true
27
+ end
28
+
29
+ def jobs
30
+ @pushed ||= []
31
+ end
32
+ end
33
+ end
34
+ end
@@ -1,6 +1,20 @@
1
+ require 'logger'
2
+
1
3
  module Sidekiq
2
4
  module Util
3
5
 
6
+ def self.logger
7
+ @logger ||= begin
8
+ log = Logger.new(STDERR)
9
+ log.level = Logger::INFO
10
+ log
11
+ end
12
+ end
13
+
14
+ def self.logger=(log)
15
+ @logger = (log ? log : Logger.new('/dev/null'))
16
+ end
17
+
4
18
  def constantize(camel_cased_word)
5
19
  names = camel_cased_word.split('::')
6
20
  names.shift if names.empty? || names.first.empty?
@@ -15,22 +29,17 @@ module Sidekiq
15
29
  def watchdog(last_words)
16
30
  yield
17
31
  rescue => ex
18
- STDERR.puts last_words
19
- STDERR.puts ex
20
- STDERR.puts ex.backtrace.join("\n")
21
- end
22
-
23
- def err(msg)
24
- STDERR.puts(msg)
32
+ logger.error last_words
33
+ logger.error ex
34
+ logger.error ex.backtrace.join("\n")
25
35
  end
26
36
 
27
- def log(msg)
28
- STDOUT.puts(msg) unless $TESTING
37
+ def logger
38
+ Sidekiq::Util.logger
29
39
  end
30
40
 
31
- def verbose(msg)
32
- STDOUT.puts(msg) if $DEBUG
41
+ def redis
42
+ Sidekiq::Manager.redis
33
43
  end
34
-
35
44
  end
36
45
  end
@@ -1,3 +1,3 @@
1
1
  module Sidekiq
2
- VERSION = "0.5.1"
2
+ VERSION = "0.6.0"
3
3
  end
@@ -24,19 +24,18 @@ module Sidekiq
24
24
  base.extend(ClassMethods)
25
25
  end
26
26
 
27
- def info(msg)
28
- print "#{msg}\n"
29
- end
30
- alias_method :log, :info
31
-
32
- def debug(msg)
33
- print "#{msg}\n" if $DEBUG
27
+ def logger
28
+ Sidekiq::Util.logger
34
29
  end
35
30
 
36
31
  module ClassMethods
37
32
  def perform_async(*args)
38
33
  Sidekiq::Client.push('class' => self.name, 'args' => args)
39
34
  end
35
+
36
+ def queue(name)
37
+ Sidekiq::Client.queues[self.name] = name.to_s
38
+ end
40
39
  end
41
40
  end
42
41
  end