sidekiq 2.17.8 → 3.0.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 (74) hide show
  1. checksums.yaml +4 -4
  2. data/3.0-Upgrade.md +63 -0
  3. data/Changes.md +66 -3
  4. data/Contributing.md +1 -3
  5. data/Pro-Changes.md +18 -0
  6. data/README.md +2 -2
  7. data/bin/sidekiqctl +19 -6
  8. data/lib/sidekiq.rb +53 -11
  9. data/lib/sidekiq/actor.rb +1 -0
  10. data/lib/sidekiq/api.rb +145 -58
  11. data/lib/sidekiq/cli.rb +22 -18
  12. data/lib/sidekiq/client.rb +44 -14
  13. data/lib/sidekiq/core_ext.rb +5 -8
  14. data/lib/sidekiq/exception_handler.rb +19 -28
  15. data/lib/sidekiq/fetch.rb +3 -3
  16. data/lib/sidekiq/launcher.rb +30 -3
  17. data/lib/sidekiq/logging.rb +2 -2
  18. data/lib/sidekiq/manager.rb +19 -16
  19. data/lib/sidekiq/middleware/chain.rb +1 -1
  20. data/lib/sidekiq/middleware/i18n.rb +1 -1
  21. data/lib/sidekiq/middleware/server/retry_jobs.rb +23 -7
  22. data/lib/sidekiq/processor.rb +36 -54
  23. data/lib/sidekiq/redis_connection.rb +1 -3
  24. data/lib/sidekiq/util.rb +4 -4
  25. data/lib/sidekiq/version.rb +1 -1
  26. data/lib/sidekiq/web.rb +57 -8
  27. data/lib/sidekiq/web_helpers.rb +6 -15
  28. data/lib/sidekiq/worker.rb +3 -1
  29. data/sidekiq.gemspec +5 -5
  30. data/test/test_api.rb +59 -19
  31. data/test/test_cli.rb +1 -1
  32. data/test/test_client.rb +44 -5
  33. data/test/test_exception_handler.rb +4 -87
  34. data/test/test_middleware.rb +3 -2
  35. data/test/test_redis_connection.rb +0 -6
  36. data/test/test_retry.rb +13 -68
  37. data/test/test_scheduled.rb +1 -1
  38. data/test/test_scheduling.rb +5 -0
  39. data/test/test_sidekiq.rb +18 -0
  40. data/test/test_web.rb +98 -58
  41. data/web/assets/stylesheets/application.css +5 -0
  42. data/web/locales/cs.yml +68 -0
  43. data/web/locales/da.yml +9 -1
  44. data/web/locales/de.yml +15 -7
  45. data/web/locales/el.yml +68 -0
  46. data/web/locales/en.yml +8 -3
  47. data/web/locales/es.yml +9 -1
  48. data/web/locales/fr.yml +34 -26
  49. data/web/locales/it.yml +26 -18
  50. data/web/locales/ja.yml +8 -2
  51. data/web/locales/ko.yml +0 -2
  52. data/web/locales/nl.yml +8 -3
  53. data/web/locales/no.yml +9 -3
  54. data/web/locales/pl.yml +0 -1
  55. data/web/locales/pt-br.yml +11 -4
  56. data/web/locales/pt.yml +8 -1
  57. data/web/locales/ru.yml +29 -22
  58. data/web/locales/sv.yml +68 -0
  59. data/web/locales/zh-tw.yml +68 -0
  60. data/web/views/_job_info.erb +8 -2
  61. data/web/views/_summary.erb +13 -7
  62. data/web/views/busy.erb +55 -0
  63. data/web/views/dead.erb +30 -0
  64. data/web/views/layout.erb +1 -0
  65. data/web/views/morgue.erb +66 -0
  66. metadata +29 -30
  67. data/config.ru +0 -18
  68. data/lib/sidekiq/capistrano.rb +0 -5
  69. data/lib/sidekiq/capistrano2.rb +0 -54
  70. data/lib/sidekiq/tasks/sidekiq.rake +0 -119
  71. data/lib/sidekiq/yaml_patch.rb +0 -21
  72. data/test/test_util.rb +0 -18
  73. data/web/views/_workers.erb +0 -22
  74. data/web/views/workers.erb +0 -16
@@ -11,7 +11,9 @@ module Sidekiq
11
11
  # processes it. It instantiates the worker, runs the middleware
12
12
  # chain and then calls Sidekiq::Worker#perform.
13
13
  class Processor
14
- STATS_TIMEOUT = 180 * 24 * 60 * 60
14
+ # To prevent a memory leak, ensure that stats expire. However, they should take up a minimal amount of storage
15
+ # so keep them around for a long time
16
+ STATS_TIMEOUT = 24 * 60 * 60 * 365 * 5
15
17
 
16
18
  include Util
17
19
  include Actor
@@ -34,32 +36,30 @@ module Sidekiq
34
36
  msgstr = work.message
35
37
  queue = work.queue_name
36
38
 
37
- do_defer do
38
- @boss.async.real_thread(proxy_id, Thread.current)
39
+ @boss.async.real_thread(proxy_id, Thread.current)
39
40
 
40
- ack = true
41
- begin
42
- msg = Sidekiq.load_json(msgstr)
43
- klass = msg['class'].constantize
44
- worker = klass.new
45
- worker.jid = msg['jid']
46
-
47
- stats(worker, msg, queue) do
48
- Sidekiq.server_middleware.invoke(worker, msg, queue) do
49
- worker.perform(*cloned(msg['args']))
50
- end
41
+ ack = true
42
+ begin
43
+ msg = Sidekiq.load_json(msgstr)
44
+ klass = msg['class'].constantize
45
+ worker = klass.new
46
+ worker.jid = msg['jid']
47
+
48
+ stats(worker, msg, queue) do
49
+ Sidekiq.server_middleware.invoke(worker, msg, queue) do
50
+ worker.perform(*cloned(msg['args']))
51
51
  end
52
- rescue Sidekiq::Shutdown
53
- # Had to force kill this job because it didn't finish
54
- # within the timeout. Don't acknowledge the work since
55
- # we didn't properly finish it.
56
- ack = false
57
- rescue Exception => ex
58
- handle_exception(ex, msg || { :message => msgstr })
59
- raise
60
- ensure
61
- work.acknowledge if ack
62
52
  end
53
+ rescue Sidekiq::Shutdown
54
+ # Had to force kill this job because it didn't finish
55
+ # within the timeout. Don't acknowledge the work since
56
+ # we didn't properly finish it.
57
+ ack = false
58
+ rescue Exception => ex
59
+ handle_exception(ex, msg || { :message => msgstr })
60
+ raise
61
+ ensure
62
+ work.acknowledge if ack
63
63
  end
64
64
 
65
65
  @boss.async.processor_done(current_actor)
@@ -71,35 +71,19 @@ module Sidekiq
71
71
 
72
72
  private
73
73
 
74
- # We use Celluloid's defer to workaround tiny little
75
- # Fiber stacks (4kb!) in MRI 1.9.
76
- #
77
- # For some reason, Celluloid's thread dispatch, TaskThread,
78
- # is unstable under heavy concurrency but TaskFiber has proven
79
- # itself stable.
80
- NEED_DEFER = (RUBY_ENGINE == 'ruby' && RUBY_VERSION < '2.0.0')
81
-
82
- def do_defer(&block)
83
- if NEED_DEFER
84
- defer(&block)
85
- else
86
- yield
87
- end
88
- end
89
-
90
- def identity
91
- @str ||= "#{hostname}:#{process_id}-#{Thread.current.object_id}:default"
74
+ def thread_identity
75
+ @str ||= Thread.current.object_id.to_s(36)
92
76
  end
93
77
 
94
78
  def stats(worker, msg, queue)
95
- # Do not conflate errors from the job with errors caused by updating stats so calling code can react appropriately
79
+ # Do not conflate errors from the job with errors caused by updating
80
+ # stats so calling code can react appropriately
96
81
  retry_and_suppress_exceptions do
97
- redis do |conn|
82
+ hash = Sidekiq.dump_json({:queue => queue, :payload => msg, :run_at => Time.now.to_i })
83
+ Sidekiq.redis do |conn|
98
84
  conn.multi do
99
- conn.sadd('workers', identity)
100
- conn.setex("worker:#{identity}:started", EXPIRY, Time.now.to_s)
101
- hash = {:queue => queue, :payload => msg, :run_at => Time.now.to_i }
102
- conn.setex("worker:#{identity}", EXPIRY, Sidekiq.dump_json(hash))
85
+ conn.hmset("#{identity}:workers", thread_identity, hash)
86
+ conn.expire("#{identity}:workers", 60*60)
103
87
  end
104
88
  end
105
89
  end
@@ -108,7 +92,7 @@ module Sidekiq
108
92
  yield
109
93
  rescue Exception
110
94
  retry_and_suppress_exceptions do
111
- redis do |conn|
95
+ Sidekiq.redis do |conn|
112
96
  failed = "stat:failed:#{Time.now.utc.to_date}"
113
97
  result = conn.multi do
114
98
  conn.incrby("stat:failed", 1)
@@ -120,12 +104,10 @@ module Sidekiq
120
104
  raise
121
105
  ensure
122
106
  retry_and_suppress_exceptions do
123
- redis do |conn|
107
+ Sidekiq.redis do |conn|
124
108
  processed = "stat:processed:#{Time.now.utc.to_date}"
125
109
  result = conn.multi do
126
- conn.srem("workers", identity)
127
- conn.del("worker:#{identity}")
128
- conn.del("worker:#{identity}:started")
110
+ conn.hdel("#{identity}:workers", thread_identity)
129
111
  conn.incrby("stat:processed", 1)
130
112
  conn.incrby(processed, 1)
131
113
  end
@@ -158,7 +140,7 @@ module Sidekiq
158
140
  sleep(1)
159
141
  retry
160
142
  else
161
- Sidekiq.logger.info {"Exhausted #{max_retries} retries due to Redis timeouts: #{e.inspect}"}
143
+ handle_exception(e, { :message => "Exhausted #{max_retries} retries"})
162
144
  end
163
145
  end
164
146
  end
@@ -68,9 +68,7 @@ module Sidekiq
68
68
  end
69
69
 
70
70
  def determine_redis_provider
71
- # REDISTOGO_URL is only support for legacy reasons
72
- provider = ENV['REDIS_PROVIDER'] || 'REDIS_URL'
73
- ENV[provider] || ENV['REDISTOGO_URL']
71
+ ENV[ENV['REDIS_PROVIDER'] || 'REDIS_URL']
74
72
  end
75
73
 
76
74
  end
@@ -26,12 +26,12 @@ module Sidekiq
26
26
  Sidekiq.redis(&block)
27
27
  end
28
28
 
29
- def process_id
30
- @@process_id ||= SecureRandom.hex
31
- end
32
-
33
29
  def hostname
34
30
  Socket.gethostname
35
31
  end
32
+
33
+ def identity
34
+ @@identity ||= "#{hostname}:#{$$}"
35
+ end
36
36
  end
37
37
  end
@@ -1,3 +1,3 @@
1
1
  module Sidekiq
2
- VERSION = "2.17.8"
2
+ VERSION = "3.0.0"
3
3
  end
@@ -20,10 +20,11 @@ module Sidekiq
20
20
 
21
21
  DEFAULT_TABS = {
22
22
  "Dashboard" => '',
23
- "Workers" => 'workers',
23
+ "Busy" => 'busy',
24
24
  "Queues" => 'queues',
25
25
  "Retries" => 'retries',
26
26
  "Scheduled" => 'scheduled',
27
+ "Dead" => 'morgue',
27
28
  }
28
29
 
29
30
  class << self
@@ -37,8 +38,8 @@ module Sidekiq
37
38
  alias_method :tabs, :custom_tabs
38
39
  end
39
40
 
40
- get "/workers" do
41
- erb :workers
41
+ get "/busy" do
42
+ erb :busy
42
43
  end
43
44
 
44
45
  get "/queues" do
@@ -55,11 +56,6 @@ module Sidekiq
55
56
  erb :queue
56
57
  end
57
58
 
58
- post "/reset" do
59
- reset_worker_list
60
- redirect root_path
61
- end
62
-
63
59
  post "/queues/:name" do
64
60
  Sidekiq::Queue.new(params[:name]).clear
65
61
  redirect "#{root_path}queues"
@@ -70,6 +66,59 @@ module Sidekiq
70
66
  redirect_with_query("#{root_path}queues/#{params[:name]}")
71
67
  end
72
68
 
69
+ get '/morgue' do
70
+ @count = (params[:count] || 25).to_i
71
+ (@current_page, @total_size, @dead) = page("dead", params[:page], @count)
72
+ @dead = @dead.map {|msg, score| Sidekiq::SortedEntry.new(nil, score, msg) }
73
+ erb :morgue
74
+ end
75
+
76
+ get "/morgue/:key" do
77
+ halt 404 unless params['key']
78
+ @dead = Sidekiq::DeadSet.new.fetch(*parse_params(params['key'])).first
79
+ redirect "#{root_path}morgue" if @dead.nil?
80
+ erb :dead
81
+ end
82
+
83
+ post '/morgue' do
84
+ halt 404 unless params['key']
85
+
86
+ params['key'].each do |key|
87
+ job = Sidekiq::DeadSet.new.fetch(*parse_params(key)).first
88
+ next unless job
89
+ if params['retry']
90
+ job.retry
91
+ elsif params['delete']
92
+ job.delete
93
+ end
94
+ end
95
+ redirect_with_query("#{root_path}morgue")
96
+ end
97
+
98
+ post "/morgue/all/delete" do
99
+ Sidekiq::DeadSet.new.clear
100
+ redirect "#{root_path}morgue"
101
+ end
102
+
103
+ post "/morgue/all/retry" do
104
+ Sidekiq::DeadSet.new.retry_all
105
+ redirect "#{root_path}morgue"
106
+ end
107
+
108
+ post "/morgue/:key" do
109
+ halt 404 unless params['key']
110
+ job = Sidekiq::DeadSet.new.fetch(*parse_params(params['key'])).first
111
+ if job
112
+ if params['retry']
113
+ job.retry
114
+ elsif params['delete']
115
+ job.delete
116
+ end
117
+ end
118
+ redirect_with_query("#{root_path}morgue")
119
+ end
120
+
121
+
73
122
  get '/retries' do
74
123
  @count = (params[:count] || 25).to_i
75
124
  (@current_page, @total_size, @retries) = page("retry", params[:page], @count)
@@ -31,28 +31,19 @@ module Sidekiq
31
31
 
32
32
  def t(msg, options={})
33
33
  string = get_locale[msg] || msg
34
- string % options
35
- end
36
-
37
- def reset_worker_list
38
- Sidekiq.redis do |conn|
39
- workers = conn.smembers('workers')
40
- conn.srem('workers', workers) if !workers.empty?
34
+ if options.empty?
35
+ string
36
+ else
37
+ string % options
41
38
  end
42
39
  end
43
40
 
44
41
  def workers_size
45
- @workers_size ||= Sidekiq.redis do |conn|
46
- conn.scard('workers')
47
- end
42
+ @workers_size ||= workers.size
48
43
  end
49
44
 
50
45
  def workers
51
- @workers ||= begin
52
- Sidekiq::Workers.new.tap do |w|
53
- w.prune
54
- end
55
- end
46
+ @workers ||= Sidekiq::Workers.new
56
47
  end
57
48
 
58
49
  def stats
@@ -62,6 +62,7 @@ module Sidekiq
62
62
  # :retry - enable the RetryJobs middleware for this Worker, default *true*
63
63
  # :backtrace - whether to save any error backtrace in the retry payload to display in web UI,
64
64
  # can be true, false or an integer number of lines to save, default *false*
65
+ # :pool - use the given Redis connection pool to push this type of job to a given shard.
65
66
  def sidekiq_options(opts={})
66
67
  self.sidekiq_options_hash = get_sidekiq_options.merge((opts || {}).stringify_keys)
67
68
  ::Sidekiq.logger.warn("#{self.name} - :timeout is unsafe and support has been removed from Sidekiq, see http://bit.ly/OtYpK for details") if opts.include? :timeout
@@ -80,7 +81,8 @@ module Sidekiq
80
81
  end
81
82
 
82
83
  def client_push(item) # :nodoc:
83
- Sidekiq::Client.push(item.stringify_keys)
84
+ pool = Thread.current[:sidekiq_via_pool] || get_sidekiq_options['pool'] || Sidekiq.redis_pool
85
+ Sidekiq::Client.new(pool).push(item.stringify_keys)
84
86
  end
85
87
 
86
88
  end
@@ -14,14 +14,14 @@ Gem::Specification.new do |gem|
14
14
  gem.name = "sidekiq"
15
15
  gem.require_paths = ["lib"]
16
16
  gem.version = Sidekiq::VERSION
17
- gem.add_dependency 'redis', '~> 3.1'
18
- gem.add_dependency 'redis-namespace', '~> 1.3'
19
- gem.add_dependency 'connection_pool', '~> 2.0'
20
- gem.add_dependency 'celluloid', '0.15.2'
17
+ gem.add_dependency 'redis', '>= 3.0.6'
18
+ gem.add_dependency 'redis-namespace', '>= 1.3.1'
19
+ gem.add_dependency 'connection_pool', '>= 2.0.0'
20
+ gem.add_dependency 'celluloid', '>= 0.15.2'
21
21
  gem.add_dependency 'json'
22
22
  gem.add_development_dependency 'sinatra'
23
23
  gem.add_development_dependency 'minitest', '~> 4.2'
24
24
  gem.add_development_dependency 'rake'
25
- gem.add_development_dependency 'rails', '4.0.5'
25
+ gem.add_development_dependency 'rails', '>= 4.0.0'
26
26
  gem.add_development_dependency 'coveralls'
27
27
  end
@@ -1,6 +1,7 @@
1
1
  require 'helper'
2
2
 
3
3
  class TestApi < Sidekiq::Test
4
+
4
5
  describe "stats" do
5
6
  before do
6
7
  Sidekiq.redis {|c| c.flushdb }
@@ -160,6 +161,8 @@ class TestApi < Sidekiq::Test
160
161
  end
161
162
 
162
163
  describe 'with an empty database' do
164
+ include Sidekiq::Util
165
+
163
166
  before do
164
167
  Sidekiq.redis {|c| c.flushdb }
165
168
  end
@@ -339,6 +342,23 @@ class TestApi < Sidekiq::Test
339
342
  assert_equal 0, r.size
340
343
  end
341
344
 
345
+ it 'can enumerate processes' do
346
+ odata = { 'pid' => 123, 'hostname' => hostname, 'key' => "#{hostname}:123", 'started_at' => Time.now.to_f - 15 }
347
+ time = Time.now.to_f
348
+ Sidekiq.redis do |conn|
349
+ conn.multi do
350
+ conn.sadd('processes', odata['key'])
351
+ conn.hmset(odata['key'], 'info', Sidekiq.dump_json(odata), 'busy', 10, 'beat', time)
352
+ conn.sadd('processes', 'fake:pid')
353
+ end
354
+ end
355
+
356
+ ps = Sidekiq::ProcessSet.new.to_a
357
+ assert_equal 1, ps.size
358
+ data = ps.first
359
+ assert_equal odata.merge('busy' => 10, 'beat' => time), data
360
+ end
361
+
342
362
  it 'can enumerate workers' do
343
363
  w = Sidekiq::Workers.new
344
364
  assert_equal 0, w.size
@@ -346,38 +366,36 @@ class TestApi < Sidekiq::Test
346
366
  assert false
347
367
  end
348
368
 
349
- s = '12345'
369
+ key = "#{hostname}:#{$$}"
370
+ pdata = { 'pid' => $$, 'hostname' => hostname, 'started_at' => Time.now.to_i }
371
+ Sidekiq.redis do |conn|
372
+ conn.sadd('processes', key)
373
+ conn.hmset(key, 'info', Sidekiq.dump_json(pdata), 'busy', 0, 'beat', Time.now.to_f)
374
+ end
375
+
376
+ s = "#{key}:workers"
350
377
  data = Sidekiq.dump_json({ 'payload' => {}, 'queue' => 'default', 'run_at' => Time.now.to_i })
351
378
  Sidekiq.redis do |c|
352
- c.multi do
353
- c.sadd('workers', s)
354
- c.set("worker:#{s}", data)
355
- c.set("worker:#{s}:started", Time.now.to_s)
356
- end
379
+ c.hmset(s, '1234', data)
357
380
  end
358
381
 
359
- assert_equal 1, w.size
360
- w.each do |x, y, z|
361
- assert_equal s, x
382
+ w.each do |p, x, y|
383
+ assert_equal key, p
384
+ assert_equal "1234", x
362
385
  assert_equal 'default', y['queue']
363
- assert_equal Time.now.year, DateTime.parse(z).year
386
+ assert_equal Time.now.year, Time.at(y['run_at']).year
364
387
  end
365
388
 
366
- s = '12346'
389
+ s = "#{key}:workers"
367
390
  data = Sidekiq.dump_json({ 'payload' => {}, 'queue' => 'default', 'run_at' => (Time.now.to_i - 2*60*60) })
368
391
  Sidekiq.redis do |c|
369
392
  c.multi do
370
- c.sadd('workers', s)
371
- c.set("worker:#{s}", data)
372
- c.set("worker:#{s}:started", Time.now.to_s)
373
- c.sadd('workers', '123457')
393
+ c.hmset(s, '5678', data)
394
+ c.hmset("b#{s}", '5678', data)
374
395
  end
375
396
  end
376
397
 
377
- assert_equal 3, w.size
378
- count = w.prune
379
- assert_equal 1, w.size
380
- assert_equal 2, count
398
+ assert_equal ['1234', '5678'], w.map { |_, tid, _| tid }
381
399
  end
382
400
 
383
401
  it 'can reschedule jobs' do
@@ -396,6 +414,28 @@ class TestApi < Sidekiq::Test
396
414
  assert(retries.map { |r| r.score > (Time.now.to_f + 9) }.any?)
397
415
  end
398
416
 
417
+ it 'prunes processes which have died' do
418
+ data = { 'pid' => rand(10_000), 'hostname' => "app#{rand(1_000)}", 'started_at' => Time.now.to_f }
419
+ key = "#{data['hostname']}:#{data['pid']}"
420
+ Sidekiq.redis do |conn|
421
+ conn.sadd('processes', key)
422
+ conn.hmset(key, 'info', Sidekiq.dump_json(data), 'busy', 0, 'beat', Time.now.to_f)
423
+ end
424
+
425
+ ps = Sidekiq::ProcessSet.new
426
+ assert_equal 1, ps.size
427
+ assert_equal 1, ps.to_a.size
428
+
429
+ Sidekiq.redis do |conn|
430
+ conn.sadd('processes', "bar:987")
431
+ conn.sadd('processes', "bar:986")
432
+ end
433
+
434
+ ps = Sidekiq::ProcessSet.new
435
+ assert_equal 3, ps.size
436
+ assert_equal 1, ps.to_a.size
437
+ end
438
+
399
439
  def add_retry(jid = 'bob', at = Time.now.to_f)
400
440
  payload = Sidekiq.dump_json('class' => 'ApiWorker', 'args' => [1, 'mike'], 'queue' => 'default', 'jid' => jid, 'retry_count' => 2, 'failed_at' => Time.now.to_f)
401
441
  Sidekiq.redis do |conn|