sidekiq 2.12.0 → 2.12.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,16 @@
1
+ 2.12.1
2
+ -----------
3
+
4
+ - Force Celluloid 0.14.1 as 0.14.0 has a serious bug. [#954]
5
+ - Scheduled and Retry jobs now use Sidekiq::Client to push
6
+ jobs onto the queue, so they use client middleware. [dimko, #948]
7
+ - Record the timestamp when jobs are enqueued. Add
8
+ Sidekiq::Job#enqueued\_at to query the time. [mariovisic, #944]
9
+ - Add Sidekiq::Queue#latency - calculates diff between now and
10
+ enqueued\_at for the oldest job in the queue.
11
+ - Add testing method `perform_one` that dequeues and performs a single job.
12
+ This is mainly to aid testing jobs that spawn other jobs. [fumin, #963]
13
+
1
14
  2.12.0
2
15
  -----------
3
16
 
@@ -3,7 +3,7 @@ Sidekiq Pro Changelog
3
3
 
4
4
  Please see http://sidekiq.org/pro for more details and how to buy.
5
5
 
6
- HEAD
6
+ 1.1.0
7
7
  -----------
8
8
 
9
9
  - New `sidekiq/pro/reliable_push` which makes Sidekiq::Client resiliant
data/README.md CHANGED
@@ -27,7 +27,7 @@ Requirements
27
27
  I test on Ruby 1.9.3 and JRuby 1.7.x. Other versions/VMs are
28
28
  untested but I will do my best to support them. Ruby 1.8 is not supported.
29
29
 
30
- Redis 2.0 or greater is required.
30
+ Redis 2.4 or greater is required.
31
31
 
32
32
 
33
33
  Installation
@@ -128,6 +128,14 @@ module Sidekiq
128
128
  Sidekiq.redis { |con| con.llen(@rname) }
129
129
  end
130
130
 
131
+ def latency
132
+ entry = Sidekiq.redis do |conn|
133
+ conn.lrange(@rname, -1, -1)
134
+ end.first
135
+ return 0 unless entry
136
+ Time.now.to_f - Sidekiq.load_json(entry)['enqueued_at']
137
+ end
138
+
131
139
  def each(&block)
132
140
  page = 0
133
141
  page_size = 50
@@ -186,10 +194,18 @@ module Sidekiq
186
194
  @item['jid']
187
195
  end
188
196
 
197
+ def enqueued_at
198
+ Time.at(@item['enqueued_at'])
199
+ end
200
+
189
201
  def queue
190
202
  @queue
191
203
  end
192
204
 
205
+ def latency
206
+ Time.now.to_f - @item['enqueued_at']
207
+ end
208
+
193
209
  ##
194
210
  # Remove this job from the queue.
195
211
  def delete
@@ -234,7 +250,7 @@ module Sidekiq
234
250
  results.map do |message|
235
251
  msg = Sidekiq.load_json(message)
236
252
  msg['retry_count'] = msg['retry_count'] - 1
237
- conn.lpush("queue:#{msg['queue']}", Sidekiq.dump_json(msg))
253
+ Sidekiq::Client.push(msg)
238
254
  end
239
255
  end
240
256
  end
@@ -372,15 +388,16 @@ module Sidekiq
372
388
  # WARNING WARNING WARNING
373
389
  #
374
390
  # This is live data that can change every millisecond.
375
- # If you do #size => 5 and then expect #each to be
391
+ # If you call #size => 5 and then expect #each to be
376
392
  # called 5 times, you're going to have a bad time.
377
393
  #
378
394
  # workers = Sidekiq::Workers.new
379
395
  # workers.size => 2
380
- # workers.each do |name, work|
381
- # # name is a unique identifier per Processor instance
396
+ # workers.each do |name, work, started_at|
397
+ # # name is a unique identifier per worker
382
398
  # # work is a Hash which looks like:
383
399
  # # { 'queue' => name, 'run_at' => timestamp, 'payload' => msg }
400
+ # # started_at is a String rep of the time when the worker started working on the job
384
401
  # end
385
402
 
386
403
  class Workers
@@ -390,9 +407,12 @@ module Sidekiq
390
407
  Sidekiq.redis do |conn|
391
408
  workers = conn.smembers("workers")
392
409
  workers.each do |w|
393
- msg = conn.get("worker:#{w}")
410
+ msg, time = conn.multi do
411
+ conn.get("worker:#{w}")
412
+ conn.get("worker:#{w}:started")
413
+ end
394
414
  next unless msg
395
- block.call(w, Sidekiq.load_json(msg))
415
+ block.call(w, Sidekiq.load_json(msg), time)
396
416
  end
397
417
  end
398
418
  end
@@ -62,7 +62,7 @@ module Sidekiq
62
62
  normed = normalize_item(items)
63
63
  payloads = items['args'].map do |args|
64
64
  raise ArgumentError, "Bulk arguments must be an Array of Arrays: [[1], [2]]" if !args.is_a?(Array)
65
- process_single(items['class'], normed.merge('args' => args, 'jid' => SecureRandom.hex(12)))
65
+ process_single(items['class'], normed.merge('args' => args, 'jid' => SecureRandom.hex(12), 'enqueued_at' => Time.now.to_f))
66
66
  end.compact
67
67
 
68
68
  pushed = false
@@ -94,7 +94,10 @@ module Sidekiq
94
94
  pushed = false
95
95
  Sidekiq.redis do |conn|
96
96
  if payloads.first['at']
97
- pushed = conn.zadd('schedule', payloads.map {|hash| [hash['at'].to_s, Sidekiq.dump_json(hash)]})
97
+ pushed = conn.zadd('schedule', payloads.map do |hash|
98
+ at = hash.delete('at').to_s
99
+ [at, Sidekiq.dump_json(hash)]
100
+ end)
98
101
  else
99
102
  q = payloads.first['queue']
100
103
  to_push = payloads.map { |entry| Sidekiq.dump_json(entry) }
@@ -129,7 +132,8 @@ module Sidekiq
129
132
  normalized_item = Sidekiq::Worker::ClassMethods::DEFAULT_OPTIONS.merge(item)
130
133
  end
131
134
 
132
- normalized_item['jid'] = SecureRandom.hex(12)
135
+ normalized_item['jid'] ||= SecureRandom.hex(12)
136
+ normalized_item['enqueued_at'] = Time.now.to_f
133
137
  normalized_item
134
138
  end
135
139
 
@@ -33,7 +33,7 @@ module Sidekiq
33
33
  end
34
34
 
35
35
  def send_to_exception_notifier(msg, ex)
36
- ::ExceptionNotifier.notify_exception(ex, :data => {:message => msg})
36
+ ::ExceptionNotifier::Notifier.background_exception_notification(ex, :data => { :message => msg }).deliver
37
37
  end
38
38
  end
39
39
  end
@@ -33,15 +33,12 @@ module Sidekiq
33
33
  # going wrong between the time jobs are popped from the scheduled queue and when
34
34
  # they are pushed onto a work queue and losing the jobs.
35
35
  while message = conn.zrangebyscore(sorted_set, '-inf', now, :limit => [0, 1]).first do
36
- msg = Sidekiq.load_json(message)
36
+
37
37
  # Pop item off the queue and add it to the work queue. If the job can't be popped from
38
38
  # the queue, it's because another process already popped it so we can move on to the
39
39
  # next one.
40
40
  if conn.zrem(sorted_set, message)
41
- conn.multi do
42
- conn.sadd('queues', msg['queue'])
43
- conn.lpush("queue:#{msg['queue']}", message)
44
- end
41
+ Sidekiq::Client.push(Sidekiq.load_json(message))
45
42
  logger.debug { "enqueued #{sorted_set}: #{message}" }
46
43
  end
47
44
  end
@@ -1,5 +1,7 @@
1
1
  module Sidekiq
2
2
 
3
+ class EmptyQueueError < RuntimeError; end
4
+
3
5
  class Client
4
6
  class << self
5
7
  alias_method :raw_push_old, :raw_push
@@ -87,6 +89,13 @@ module Sidekiq
87
89
  new.perform(*job['args'])
88
90
  end
89
91
  end
92
+
93
+ # Pop out a single job and perform it
94
+ def perform_one
95
+ raise(EmptyQueueError, "perform_one called with empty job queue") if jobs.empty?
96
+ job = jobs.shift
97
+ new.perform(*job['args'])
98
+ end
90
99
  end
91
100
 
92
101
  class << self
@@ -1,3 +1,3 @@
1
1
  module Sidekiq
2
- VERSION = "2.12.0"
2
+ VERSION = "2.12.1"
3
3
  end
@@ -2,6 +2,8 @@ require 'yaml'
2
2
  require 'sinatra/base'
3
3
  require 'slim'
4
4
 
5
+ raise "The Sidekiq Web UI requires slim 1.3.8 or greater. You have slim v#{Slim::VERSION}" if Gem::Version.new(Slim::VERSION) < Gem::Version.new('1.3.8')
6
+
5
7
  require 'sidekiq'
6
8
  require 'sidekiq/api'
7
9
  require 'sidekiq/paginator'
@@ -105,6 +107,20 @@ module Sidekiq
105
107
  args.map { |arg| a = arg.inspect; a.size > count ? "#{a[0..count]}..." : a }.join(", ")
106
108
  end
107
109
 
110
+ RETRY_JOB_KEYS = Set.new(%w(
111
+ queue class args retry_count retried_at failed_at
112
+ jid error_message error_class backtrace
113
+ error_backtrace enqueued_at
114
+ ))
115
+
116
+ def retry_extra_items(retry_job)
117
+ @retry_extra_items ||= {}.tap do |extra|
118
+ retry_job.item.each do |key, value|
119
+ extra[key] = value unless RETRY_JOB_KEYS.include?(key)
120
+ end
121
+ end
122
+ end
123
+
108
124
  def tabs
109
125
  @tabs ||= {
110
126
  "Dashboard" => '',
@@ -17,7 +17,7 @@ Gem::Specification.new do |gem|
17
17
  gem.add_dependency 'redis', '>= 3.0'
18
18
  gem.add_dependency 'redis-namespace'
19
19
  gem.add_dependency 'connection_pool', '>= 1.0.0'
20
- gem.add_dependency 'celluloid', '>= 0.14.0'
20
+ gem.add_dependency 'celluloid', '>= 0.14.1'
21
21
  gem.add_dependency 'json'
22
22
  gem.add_development_dependency 'sinatra'
23
23
  gem.add_development_dependency 'slim'
@@ -164,6 +164,7 @@ class TestApi < Minitest::Test
164
164
  it 'shows queue as empty' do
165
165
  q = Sidekiq::Queue.new
166
166
  assert_equal 0, q.size
167
+ assert_equal 0, q.latency
167
168
  end
168
169
 
169
170
  class ApiWorker
@@ -172,12 +173,18 @@ class TestApi < Minitest::Test
172
173
 
173
174
  it 'can enumerate jobs' do
174
175
  q = Sidekiq::Queue.new
175
- ApiWorker.perform_async(1, 'mike')
176
- assert_equal ['TestApi::ApiWorker'], q.map(&:klass)
176
+ Time.stub(:now, Time.new(2012, 12, 26)) do
177
+ ApiWorker.perform_async(1, 'mike')
178
+ assert_equal ['TestApi::ApiWorker'], q.map(&:klass)
179
+
180
+ job = q.first
181
+ assert_equal 24, job.jid.size
182
+ assert_equal [1, 'mike'], job.args
183
+ assert_equal Time.new(2012, 12, 26), job.enqueued_at
177
184
 
178
- job = q.first
179
- assert_equal 24, job.jid.size
180
- assert_equal [1, 'mike'], job.args
185
+ end
186
+
187
+ assert q.latency > 10_000_000
181
188
 
182
189
  q = Sidekiq::Queue.new('other')
183
190
  assert_equal 0, q.size
@@ -192,11 +199,11 @@ class TestApi < Minitest::Test
192
199
  end
193
200
 
194
201
  it 'can find job by id in sorted sets' do
195
- q = Sidekiq::Queue.new
196
202
  job_id = ApiWorker.perform_in(100, 1, 'jason')
197
203
  job = Sidekiq::ScheduledSet.new.find_job(job_id)
198
204
  refute_nil job
199
205
  assert_equal job_id, job.jid
206
+ assert_in_delta job.latency, 0.0, 0.01
200
207
  end
201
208
 
202
209
  it 'can find job by id in queues' do
@@ -310,13 +317,15 @@ class TestApi < Minitest::Test
310
317
  c.multi do
311
318
  c.sadd('workers', s)
312
319
  c.set("worker:#{s}", data)
320
+ c.set("worker:#{s}:started", Time.now.to_s)
313
321
  end
314
322
  end
315
323
 
316
324
  assert_equal 1, w.size
317
- w.each do |x, y|
325
+ w.each do |x, y, z|
318
326
  assert_equal s, x
319
327
  assert_equal 'default', y['queue']
328
+ assert_equal Time.now.year, DateTime.parse(z).year
320
329
  end
321
330
  end
322
331
 
@@ -73,7 +73,8 @@ class TestExceptionHandler < Minitest::Test
73
73
 
74
74
  describe "with fake ExceptionNotifier" do
75
75
  before do
76
- ::ExceptionNotifier = Minitest::Mock.new
76
+ ::ExceptionNotifier = Module.new
77
+ ::ExceptionNotifier::Notifier = MiniTest::Mock.new
77
78
  end
78
79
 
79
80
  after do
@@ -81,9 +82,12 @@ class TestExceptionHandler < Minitest::Test
81
82
  end
82
83
 
83
84
  it "notifies ExceptionNotifier" do
84
- ::ExceptionNotifier.expect(:notify_exception,nil,[TEST_EXCEPTION, :data => { :message => { :b => 2 } }])
85
+ mail = MiniTest::Mock.new
86
+ mail.expect(:deliver,nil)
87
+ ::ExceptionNotifier::Notifier.expect(:background_exception_notification,mail,[TEST_EXCEPTION, :data => { :message => { :b => 2 } }])
85
88
  Component.new.invoke_exception(:b => 2)
86
- ::ExceptionNotifier.verify
89
+ ::ExceptionNotifier::Notifier.verify
90
+ mail.verify
87
91
  end
88
92
  end
89
93
 
@@ -14,33 +14,72 @@ class TestScheduled < Minitest::Test
14
14
  Sidekiq.redis do |conn|
15
15
  conn.flushdb
16
16
  end
17
+
18
+ @error_1 = { 'class' => ScheduledWorker.name, 'args' => [0], 'queue' => 'queue_1' }
19
+ @error_2 = { 'class' => ScheduledWorker.name, 'args' => [1], 'queue' => 'queue_2' }
20
+ @error_3 = { 'class' => ScheduledWorker.name, 'args' => [2], 'queue' => 'queue_3' }
21
+ @future_1 = { 'class' => ScheduledWorker.name, 'args' => [3], 'queue' => 'queue_4' }
22
+ @future_2 = { 'class' => ScheduledWorker.name, 'args' => [4], 'queue' => 'queue_5' }
23
+ @future_3 = { 'class' => ScheduledWorker.name, 'args' => [5], 'queue' => 'queue_6' }
24
+
25
+ @retry = Sidekiq::RetrySet.new
26
+ @scheduled = Sidekiq::ScheduledSet.new
27
+ @poller = Sidekiq::Scheduled::Poller.new
28
+ end
29
+
30
+ class Stopper
31
+ def call(worker_class, message, queue)
32
+ yield if message['args'].first.odd?
33
+ end
34
+ end
35
+
36
+ it 'executes client middleware' do
37
+ Sidekiq.client_middleware.add Stopper
38
+ begin
39
+ @retry.schedule (Time.now - 60).to_f, @error_1
40
+ @retry.schedule (Time.now - 60).to_f, @error_2
41
+ @scheduled.schedule (Time.now - 60).to_f, @future_2
42
+ @scheduled.schedule (Time.now - 60).to_f, @future_3
43
+
44
+ @poller.poll
45
+
46
+ Sidekiq.redis do |conn|
47
+ assert_equal 0, conn.llen("queue:queue_1")
48
+ assert_equal 1, conn.llen("queue:queue_2")
49
+ assert_equal 0, conn.llen("queue:queue_5")
50
+ assert_equal 1, conn.llen("queue:queue_6")
51
+ end
52
+ ensure
53
+ Sidekiq.client_middleware.remove Stopper
54
+ end
17
55
  end
18
56
 
19
57
  it 'should empty the retry and scheduled queues up to the current time' do
20
- Sidekiq.redis do |conn|
21
- error_1 = Sidekiq.dump_json('class' => ScheduledWorker.name, 'args' => ["error_1"], 'queue' => 'queue_1')
22
- error_2 = Sidekiq.dump_json('class' => ScheduledWorker.name, 'args' => ["error_2"], 'queue' => 'queue_2')
23
- error_3 = Sidekiq.dump_json('class' => ScheduledWorker.name, 'args' => ["error_3"], 'queue' => 'queue_3')
24
- future_1 = Sidekiq.dump_json('class' => ScheduledWorker.name, 'args' => ["future_1"], 'queue' => 'queue_4')
25
- future_2 = Sidekiq.dump_json('class' => ScheduledWorker.name, 'args' => ["future_2"], 'queue' => 'queue_5')
26
- future_3 = Sidekiq.dump_json('class' => ScheduledWorker.name, 'args' => ["future_3"], 'queue' => 'queue_6')
27
-
28
- conn.zadd("retry", (Time.now - 60).to_f.to_s, error_1)
29
- conn.zadd("retry", (Time.now - 50).to_f.to_s, error_2)
30
- conn.zadd("retry", (Time.now + 60).to_f.to_s, error_3)
31
- conn.zadd("schedule", (Time.now - 60).to_f.to_s, future_1)
32
- conn.zadd("schedule", (Time.now - 50).to_f.to_s, future_2)
33
- conn.zadd("schedule", (Time.now + 60).to_f.to_s, future_3)
34
-
35
- poller = Sidekiq::Scheduled::Poller.new
36
- poller.poll
37
-
38
- assert_equal [error_1], conn.lrange("queue:queue_1", 0, -1)
39
- assert_equal [error_2], conn.lrange("queue:queue_2", 0, -1)
40
- assert_equal [error_3], conn.zrange("retry", 0, -1)
41
- assert_equal [future_1], conn.lrange("queue:queue_4", 0, -1)
42
- assert_equal [future_2], conn.lrange("queue:queue_5", 0, -1)
43
- assert_equal [future_3], conn.zrange("schedule", 0, -1)
58
+ enqueued_time = Time.new(2013, 2, 4)
59
+
60
+ Time.stub(:now, enqueued_time) do
61
+ @retry.schedule (Time.now - 60).to_f, @error_1
62
+ @retry.schedule (Time.now - 50).to_f, @error_2
63
+ @retry.schedule (Time.now + 60).to_f, @error_3
64
+ @scheduled.schedule (Time.now - 60).to_f, @future_1
65
+ @scheduled.schedule (Time.now - 50).to_f, @future_2
66
+ @scheduled.schedule (Time.now + 60).to_f, @future_3
67
+
68
+ @poller.poll
69
+
70
+ Sidekiq.redis do |conn|
71
+ assert_equal 1, conn.llen("queue:queue_1")
72
+ assert_equal enqueued_time.to_f, Sidekiq.load_json(conn.lrange("queue:queue_1", 0, -1)[0])['enqueued_at']
73
+ assert_equal 1, conn.llen("queue:queue_2")
74
+ assert_equal enqueued_time.to_f, Sidekiq.load_json(conn.lrange("queue:queue_2", 0, -1)[0])['enqueued_at']
75
+ assert_equal 1, conn.llen("queue:queue_4")
76
+ assert_equal enqueued_time.to_f, Sidekiq.load_json(conn.lrange("queue:queue_4", 0, -1)[0])['enqueued_at']
77
+ assert_equal 1, conn.llen("queue:queue_5")
78
+ assert_equal enqueued_time.to_f, Sidekiq.load_json(conn.lrange("queue:queue_5", 0, -1)[0])['enqueued_at']
79
+ end
80
+
81
+ assert_equal 1, @retry.size
82
+ assert_equal 1, @scheduled.size
44
83
  end
45
84
  end
46
85
  end
@@ -119,6 +119,23 @@ class TestTesting < Minitest::Test
119
119
  StoredWorker.clear
120
120
  end
121
121
 
122
+ it 'perform_one runs only one job' do
123
+ DirectWorker.perform_async(1, 2)
124
+ DirectWorker.perform_async(3, 4)
125
+ assert_equal 2, DirectWorker.jobs.size
126
+
127
+ DirectWorker.perform_one
128
+ assert_equal 1, DirectWorker.jobs.size
129
+
130
+ DirectWorker.clear
131
+ end
132
+
133
+ it 'perform_one raise error upon empty queue' do
134
+ assert_raises Sidekiq::EmptyQueueError do
135
+ DirectWorker.perform_one
136
+ end
137
+ end
138
+
122
139
  class FirstWorker
123
140
  include Sidekiq::Worker
124
141
  class_attribute :count
@@ -264,7 +264,6 @@ class TestWeb < Minitest::Test
264
264
  score = Time.now.to_f
265
265
  msg = { 'class' => 'HardWorker',
266
266
  'args' => ['bob', 1, Time.now.to_f],
267
- 'at' => score,
268
267
  'jid' => 'f39af2a05e8f4b24dbc0f1e4' }
269
268
  Sidekiq.redis do |conn|
270
269
  conn.zadd('schedule', score, Sidekiq.dump_json(msg))
@@ -21,6 +21,7 @@ en: # <---- change this to your locale code
21
21
  Class: Class
22
22
  Job: Job
23
23
  Arguments: Arguments
24
+ Extras: Extras
24
25
  Started: Started
25
26
  ShowAll: Show All
26
27
  CurrentMessagesInQueue: Current messages in <span class='title'>%{queue}</span>
@@ -20,6 +20,7 @@
20
20
  Class: Classe
21
21
  Job: Tarefa
22
22
  Arguments: Argumentos
23
+ Extras: Extras
23
24
  Started: Iniciados
24
25
  ShowAll: Mostrar todos
25
26
  CurrentMessagesInQueue: Mensagens atualmenta na <span class='title'>%{queue}</span>
@@ -19,6 +19,11 @@ table class="retry table table-bordered table-striped"
19
19
  th JID
20
20
  td
21
21
  code= @retry.jid
22
+ - unless retry_extra_items(@retry).empty?
23
+ tr
24
+ th = t('Extras')
25
+ td
26
+ code= display_args(retry_extra_items(@retry), 1000)
22
27
  - if @retry['retry_count'] > 0
23
28
  tr
24
29
  th = t('RetryCount')
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.12.0
4
+ version: 2.12.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: 2013-05-15 00:00:00.000000000 Z
12
+ date: 2013-05-31 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: redis
@@ -66,7 +66,7 @@ dependencies:
66
66
  requirements:
67
67
  - - ! '>='
68
68
  - !ruby/object:Gem::Version
69
- version: 0.14.0
69
+ version: 0.14.1
70
70
  type: :runtime
71
71
  prerelease: false
72
72
  version_requirements: !ruby/object:Gem::Requirement
@@ -74,7 +74,7 @@ dependencies:
74
74
  requirements:
75
75
  - - ! '>='
76
76
  - !ruby/object:Gem::Version
77
- version: 0.14.0
77
+ version: 0.14.1
78
78
  - !ruby/object:Gem::Dependency
79
79
  name: json
80
80
  requirement: !ruby/object:Gem::Requirement