sidekiq 2.3.0 → 2.3.1

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,11 @@
1
+ 2.3.1
2
+ -----------
3
+
4
+ - Add Sidekiq::Client.push\_bulk for bulk adding of jobs to Redis.
5
+ My own simple test case shows pushing 10,000 jobs goes from 5 sec to 1.5 sec.
6
+ - Add support for multiple processes per host to Capistrano recipe
7
+ - Re-enable Celluloid::Actor#defer to fix stack overflow issues [#398]
8
+
1
9
  2.3.0
2
10
  -----------
3
11
 
@@ -14,6 +14,7 @@ require 'sidekiq/rails' if defined?(::Rails::Engine)
14
14
  require 'multi_json'
15
15
 
16
16
  module Sidekiq
17
+ NAME = "Sidekiq"
17
18
  LICENSE = 'See LICENSE and the LGPL-3.0 for licensing details.'
18
19
 
19
20
  DEFAULTS = {
@@ -4,26 +4,38 @@ Capistrano::Configuration.instance.load do
4
4
  after "deploy:start", "sidekiq:start"
5
5
  after "deploy:restart", "sidekiq:restart"
6
6
 
7
- _cset(:sidekiq_timeout) { 10 }
8
- _cset(:sidekiq_role) { :app }
9
- _cset(:sidekiq_pid) { "#{current_path}/tmp/pids/sidekiq.pid" }
7
+ _cset(:sidekiq_timeout) { 10 }
8
+ _cset(:sidekiq_role) { :app }
9
+ _cset(:sidekiq_pid) { "#{current_path}/tmp/pids/sidekiq.pid" }
10
+ _cset(:sidekiq_processes) { 1 }
10
11
 
11
12
  namespace :sidekiq do
13
+ def for_each_process(&block)
14
+ 0.upto(fetch(:sidekiq_processes) - 1) do |process|
15
+ yield process == 0 ? "#{fetch(:sidekiq_pid)}" : "#{fetch(:sidekiq_pid)}-#{process}"
16
+ end
17
+ end
12
18
 
13
19
  desc "Quiet sidekiq (stop accepting new work)"
14
20
  task :quiet, :roles => lambda { fetch(:sidekiq_role) }, :on_no_matching_servers => :continue do
15
- run "if [ -d #{current_path} ] && [ -f #{fetch :sidekiq_pid} ]; then cd #{current_path} && #{fetch(:bundle_cmd, "bundle")} exec sidekiqctl quiet #{fetch :sidekiq_pid} ; fi"
21
+ for_each_process do |pid_file|
22
+ run "if [ -d #{current_path} ] && [ -f #{pid_file} ]; then cd #{current_path} && #{fetch(:bundle_cmd, "bundle")} exec sidekiqctl quiet #{pid_file} ; fi"
23
+ end
16
24
  end
17
25
 
18
26
  desc "Stop sidekiq"
19
27
  task :stop, :roles => lambda { fetch(:sidekiq_role) }, :on_no_matching_servers => :continue do
20
- run "if [ -d #{current_path} ] && [ -f #{fetch :sidekiq_pid} ]; then cd #{current_path} && #{fetch(:bundle_cmd, "bundle")} exec sidekiqctl stop #{fetch :sidekiq_pid} #{fetch :sidekiq_timeout} ; fi"
28
+ for_each_process do |pid_file|
29
+ run "if [ -d #{current_path} ] && [ -f #{pid_file} ]; then cd #{current_path} && #{fetch(:bundle_cmd, "bundle")} exec sidekiqctl stop #{pid_file} #{fetch :sidekiq_timeout} ; fi"
30
+ end
21
31
  end
22
32
 
23
33
  desc "Start sidekiq"
24
34
  task :start, :roles => lambda { fetch(:sidekiq_role) }, :on_no_matching_servers => :continue do
25
35
  rails_env = fetch(:rails_env, "production")
26
- run "cd #{current_path} ; nohup #{fetch(:bundle_cmd, "bundle")} exec sidekiq -e #{rails_env} -C #{current_path}/config/sidekiq.yml -P #{fetch :sidekiq_pid} >> #{current_path}/log/sidekiq.log 2>&1 &", :pty => false
36
+ for_each_process do |pid_file|
37
+ run "cd #{current_path} ; nohup #{fetch(:bundle_cmd, "bundle")} exec sidekiq -e #{rails_env} -C #{current_path}/config/sidekiq.yml -P #{pid_file} >> #{current_path}/log/sidekiq.log 2>&1 &", :pty => false
38
+ end
27
39
  end
28
40
 
29
41
  desc "Restart sidekiq"
@@ -36,33 +36,52 @@ module Sidekiq
36
36
  # Sidekiq::Client.push('queue' => 'my_queue', 'class' => MyWorker, 'args' => ['foo', 1, :bat => 'bar'])
37
37
  #
38
38
  def self.push(item)
39
- raise(ArgumentError, "Message must be a Hash of the form: { 'class' => SomeWorker, 'args' => ['bob', 1, :foo => 'bar'] }") unless item.is_a?(Hash)
40
- raise(ArgumentError, "Message must include a class and set of arguments: #{item.inspect}") if !item['class'] || !item['args']
41
- raise(ArgumentError, "Message must include a Sidekiq::Worker class, not class name: #{item['class'].ancestors.inspect}") if !item['class'].is_a?(Class) || !item['class'].respond_to?('get_sidekiq_options')
39
+ normed = normalize_item(item)
40
+ normed, payload = process_single(item['class'], normed)
42
41
 
43
- worker_class = item['class']
44
- item['class'] = item['class'].to_s
42
+ pushed = false
43
+ Sidekiq.redis do |conn|
44
+ if normed['at']
45
+ pushed = conn.zadd('schedule', normed['at'].to_s, payload)
46
+ else
47
+ _, pushed = conn.multi do
48
+ conn.sadd('queues', normed['queue'])
49
+ conn.rpush("queue:#{normed['queue']}", payload)
50
+ end
51
+ end
52
+ end if normed
53
+ pushed ? normed['jid'] : nil
54
+ end
45
55
 
46
- item = worker_class.get_sidekiq_options.merge(item)
47
- item['retry'] = !!item['retry']
48
- queue = item['queue']
49
- item['jid'] = SecureRandom.hex(12)
56
+ ##
57
+ # Push a large number of jobs to Redis. In practice this method is only
58
+ # useful if you are pushing tens of thousands of jobs or more. This method
59
+ # basically cuts down on the redis round trip latency.
60
+ #
61
+ # Takes the same arguments as Client.push except that args is expected to be
62
+ # an Array of Arrays. All other keys are duplicated for each job. Each job
63
+ # is run through the client middleware pipeline and each job gets its own Job ID
64
+ # as normal.
65
+ #
66
+ # Returns the number of jobs pushed or nil if the pushed failed. The number of jobs
67
+ # pushed can be less than the number given if the middleware stopped processing for one
68
+ # or more jobs.
69
+ def self.push_bulk(items)
70
+ normed = normalize_item(items)
71
+ payloads = items['args'].map do |args|
72
+ _, payload = process_single(items['class'], normed.merge('args' => args, 'jid' => SecureRandom.hex(12)))
73
+ payload
74
+ end.compact
50
75
 
51
76
  pushed = false
52
- Sidekiq.client_middleware.invoke(worker_class, item, queue) do
53
- payload = Sidekiq.dump_json(item)
54
- Sidekiq.redis do |conn|
55
- if item['at']
56
- pushed = conn.zadd('schedule', item['at'].to_s, payload)
57
- else
58
- _, pushed = conn.multi do
59
- conn.sadd('queues', queue)
60
- conn.rpush("queue:#{queue}", payload)
61
- end
62
- end
77
+ Sidekiq.redis do |conn|
78
+ _, pushed = conn.multi do
79
+ conn.sadd('queues', normed['queue'])
80
+ conn.rpush("queue:#{normed['queue']}", payloads)
63
81
  end
64
82
  end
65
- pushed ? item['jid'] : nil
83
+
84
+ pushed ? payloads.size : nil
66
85
  end
67
86
 
68
87
  # Resque compatibility helpers.
@@ -82,5 +101,30 @@ module Sidekiq
82
101
  def self.enqueue_to(queue, klass, *args)
83
102
  klass.client_push('queue' => queue, 'class' => klass, 'args' => args)
84
103
  end
104
+
105
+ private
106
+
107
+ def self.process_single(worker_class, item)
108
+ queue = item['queue']
109
+
110
+ Sidekiq.client_middleware.invoke(worker_class, item, queue) do
111
+ payload = Sidekiq.dump_json(item)
112
+ return item, payload
113
+ end
114
+ end
115
+
116
+ def self.normalize_item(item)
117
+ raise(ArgumentError, "Message must be a Hash of the form: { 'class' => SomeWorker, 'args' => ['bob', 1, :foo => 'bar'] }") unless item.is_a?(Hash)
118
+ raise(ArgumentError, "Message must include a class and set of arguments: #{item.inspect}") if !item['class'] || !item['args']
119
+ raise(ArgumentError, "Message must include a Sidekiq::Worker class, not class name: #{item['class'].ancestors.inspect}") if !item['class'].is_a?(Class) || !item['class'].respond_to?('get_sidekiq_options')
120
+
121
+ normalized_item = item.dup
122
+ normalized_item['class'] = normalized_item['class'].to_s
123
+ normalized_item['retry'] = !!normalized_item['retry']
124
+ normalized_item['jid'] = SecureRandom.hex(12)
125
+
126
+ item['class'].get_sidekiq_options.merge normalized_item
127
+ end
128
+
85
129
  end
86
130
  end
@@ -1,3 +1,4 @@
1
+ require 'socket'
1
2
  require 'celluloid'
2
3
  require 'sidekiq/util'
3
4
 
@@ -31,19 +32,21 @@ module Sidekiq
31
32
  end
32
33
 
33
34
  def process(msgstr, queue)
34
- begin
35
- msg = Sidekiq.load_json(msgstr)
36
- klass = constantize(msg['class'])
37
- worker = klass.new
35
+ defer do
36
+ begin
37
+ msg = Sidekiq.load_json(msgstr)
38
+ klass = constantize(msg['class'])
39
+ worker = klass.new
38
40
 
39
- stats(worker, msg, queue) do
40
- Sidekiq.server_middleware.invoke(worker, msg, queue) do
41
- worker.perform(*cloned(msg['args']))
41
+ stats(worker, msg, queue) do
42
+ Sidekiq.server_middleware.invoke(worker, msg, queue) do
43
+ worker.perform(*cloned(msg['args']))
44
+ end
42
45
  end
46
+ rescue Exception => ex
47
+ handle_exception(ex, msg || { :message => msgstr })
48
+ raise
43
49
  end
44
- rescue Exception => ex
45
- handle_exception(ex, msg || { :message => msgstr })
46
- raise
47
50
  end
48
51
  @boss.processor_done!(current_actor)
49
52
  end
@@ -105,7 +108,7 @@ module Sidekiq
105
108
  end
106
109
 
107
110
  def hostname
108
- @h ||= `hostname`.strip
111
+ @h ||= Socket.gethostname
109
112
  end
110
113
  end
111
114
  end
@@ -1,3 +1,3 @@
1
1
  module Sidekiq
2
- VERSION = "2.3.0"
2
+ VERSION = "2.3.1"
3
3
  end
@@ -99,6 +99,14 @@ class TestClient < MiniTest::Unit::TestCase
99
99
  end
100
100
  end
101
101
 
102
+ describe 'bulk' do
103
+ it 'can push a large set of jobs at once' do
104
+ a = Time.now
105
+ count = Sidekiq::Client.push_bulk('class' => QueuedWorker, 'args' => (1..1_000).to_a.map { |x| Array(x) })
106
+ assert_equal 1_000, count
107
+ end
108
+ end
109
+
102
110
  class BaseWorker
103
111
  include Sidekiq::Worker
104
112
  sidekiq_options 'retry' => 'base'
@@ -109,6 +117,26 @@ class TestClient < MiniTest::Unit::TestCase
109
117
  sidekiq_options 'retry' => 'b'
110
118
  end
111
119
 
120
+ describe 'client middleware' do
121
+
122
+ class Stopper
123
+ def call(worker_class, message, queue)
124
+ yield if message['args'].first.odd?
125
+ end
126
+ end
127
+
128
+ it 'can stop some of the jobs from pushing' do
129
+ Sidekiq.client_middleware.add Stopper
130
+ begin
131
+ assert_equal nil, Sidekiq::Client.push('class' => MyWorker, 'args' => [0])
132
+ assert_match /[0-9a-f]{12}/, Sidekiq::Client.push('class' => MyWorker, 'args' => [1])
133
+ assert_equal 1, Sidekiq::Client.push_bulk('class' => MyWorker, 'args' => [[0], [1]])
134
+ ensure
135
+ Sidekiq.client_middleware.remove Stopper
136
+ end
137
+ end
138
+ end
139
+
112
140
  describe 'inheritance' do
113
141
  it 'should inherit sidekiq options' do
114
142
  assert_equal 'base', AWorker.get_sidekiq_options['retry']
@@ -2,7 +2,7 @@ doctype html
2
2
  html
3
3
  head
4
4
  link href='#{{root_path}}assets/application.css' media='screen' rel='stylesheet' type='text/css'
5
- title Sidekiq
5
+ title= Sidekiq::NAME
6
6
  body
7
7
  .navbar.navbar-fixed-top.navbar-inverse
8
8
  .navbar-inner
@@ -12,7 +12,7 @@ html
12
12
  span.icon-bar
13
13
  span.icon-bar
14
14
  a.brand href='#{{root_path}}'
15
- | Sidekiq
15
+ = Sidekiq::NAME
16
16
  div.nav-collapse
17
17
  ul.nav
18
18
  li
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.3.0
4
+ version: 2.3.1
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: 2012-09-09 00:00:00.000000000 Z
12
+ date: 2012-09-14 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: redis