sidekiq 2.15.1 → 2.15.2

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,10 @@
1
+ 2.15.2
2
+ -----------
3
+
4
+ - Iterating over Sidekiq::Queue and Sidekiq::SortedSet will now work as
5
+ intended when jobs are deleted [#866, aackerman]
6
+ - A few more minor Web UI fixes [#1247]
7
+
1
8
  2.15.1
2
9
  -----------
3
10
 
@@ -117,18 +117,23 @@ module Sidekiq
117
117
  end
118
118
 
119
119
  def each(&block)
120
+ initial_size = size
121
+ deleted_size = 0
120
122
  page = 0
121
123
  page_size = 50
122
124
 
123
125
  loop do
126
+ range_start = page * page_size - deleted_size
127
+ range_end = page * page_size - deleted_size + (page_size - 1)
124
128
  entries = Sidekiq.redis do |conn|
125
- conn.lrange @rname, page * page_size, (page * page_size) + page_size - 1
129
+ conn.lrange @rname, range_start, range_end
126
130
  end
127
131
  break if entries.empty?
128
132
  page += 1
129
133
  entries.each do |entry|
130
134
  block.call Job.new(entry, @name)
131
135
  end
136
+ deleted_size = initial_size - size
132
137
  end
133
138
  end
134
139
 
@@ -265,19 +270,23 @@ module Sidekiq
265
270
  end
266
271
 
267
272
  def each(&block)
268
- # page thru the sorted set backwards so deleting entries doesn't screw up indexing
273
+ initial_size = size
274
+ deleted_size = 0
269
275
  page = -1
270
276
  page_size = 50
271
277
 
272
278
  loop do
279
+ range_start = page * page_size + deleted_size
280
+ range_end = page * page_size + deleted_size + (page_size - 1)
273
281
  elements = Sidekiq.redis do |conn|
274
- conn.zrange @zset, page * page_size, (page * page_size) + (page_size - 1), :with_scores => true
282
+ conn.zrange @zset, range_start, range_end, :with_scores => true
275
283
  end
276
284
  break if elements.empty?
277
285
  page -= 1
278
286
  elements.each do |element, score|
279
287
  block.call SortedEntry.new(self, score, element)
280
288
  end
289
+ deleted_size = initial_size - size
281
290
  end
282
291
  end
283
292
 
@@ -4,7 +4,7 @@ module Sidekiq
4
4
  def handle_exception(ex, ctxHash={})
5
5
  Sidekiq.logger.warn(ctxHash) if !ctxHash.empty?
6
6
  Sidekiq.logger.warn ex
7
- Sidekiq.logger.warn ex.backtrace.join("\n")
7
+ Sidekiq.logger.warn ex.backtrace.join("\n") unless ex.backtrace.nil?
8
8
  # This list of services is getting a bit ridiculous.
9
9
  # For future error services, please add your own
10
10
  # middleware like BugSnag does:
@@ -1,3 +1,3 @@
1
1
  module Sidekiq
2
- VERSION = "2.15.1"
2
+ VERSION = "2.15.2"
3
3
  end
@@ -5,6 +5,7 @@ require 'sinatra/base'
5
5
  require 'sidekiq'
6
6
  require 'sidekiq/api'
7
7
  require 'sidekiq/paginator'
8
+ require 'sidekiq/web_helpers'
8
9
 
9
10
  module Sidekiq
10
11
  class Web < Sinatra::Base
@@ -15,166 +16,25 @@ module Sidekiq
15
16
  set :views, Proc.new { "#{root}/views" }
16
17
  set :locales, Proc.new { "#{root}/locales" }
17
18
 
18
- helpers do
19
- def strings
20
- @strings ||= begin
21
- Dir["#{settings.locales}/*.yml"].inject({}) do |memo, file|
22
- memo.merge(YAML.load(File.open(file)))
23
- end
24
- end
25
- end
26
-
27
- def locale
28
- lang = (request.env["HTTP_ACCEPT_LANGUAGE"] || 'en')[0,2]
29
- strings[lang] ? lang : 'en'
30
- end
31
-
32
- def get_locale
33
- strings[locale]
34
- end
35
-
36
- def t(msg, options={})
37
- string = get_locale[msg] || msg
38
- string % options
39
- end
40
-
41
- def reset_worker_list
42
- Sidekiq.redis do |conn|
43
- workers = conn.smembers('workers')
44
- conn.srem('workers', workers) if !workers.empty?
45
- end
46
- end
47
-
48
- def workers_size
49
- @workers_size ||= Sidekiq.redis do |conn|
50
- conn.scard('workers')
51
- end
52
- end
53
-
54
- def workers
55
- @workers ||= begin
56
- to_rem = []
57
- workers = Sidekiq.redis do |conn|
58
- conn.smembers('workers').map do |w|
59
- msg = conn.get("worker:#{w}")
60
- msg ? [w, Sidekiq.load_json(msg)] : (to_rem << w; nil)
61
- end.compact.sort { |x| x[1] ? -1 : 1 }
62
- end
63
-
64
- # Detect and clear out any orphaned worker records.
65
- # These can be left in Redis if Sidekiq crashes hard
66
- # while processing jobs.
67
- if to_rem.size > 0
68
- Sidekiq.redis { |conn| conn.srem('workers', to_rem) }
69
- end
70
- workers
71
- end
72
- end
73
-
74
- def stats
75
- @stats ||= Sidekiq::Stats.new
76
- end
77
-
78
- def retries_with_score(score)
79
- Sidekiq.redis do |conn|
80
- results = conn.zrangebyscore('retry', score, score)
81
- results.map { |msg| Sidekiq.load_json(msg) }
82
- end
83
- end
84
-
85
- def location
86
- Sidekiq.redis { |conn| conn.client.location }
87
- end
88
-
89
- def namespace
90
- @@ns ||= Sidekiq.redis {|conn| conn.respond_to?(:namespace) ? conn.namespace : nil }
91
- end
92
-
93
- def root_path
94
- "#{env['SCRIPT_NAME']}/"
95
- end
19
+ helpers WebHelpers
96
20
 
97
- def current_path
98
- @current_path ||= request.path_info.gsub(/^\//,'')
99
- end
100
-
101
- def current_status
102
- return 'idle' if workers_size == 0
103
- return 'active'
104
- end
105
-
106
- def relative_time(time)
107
- %{<time datetime="#{time.getutc.iso8601}">#{time}</time>}
108
- end
109
-
110
- def job_params(job, score)
111
- "#{score}-#{job['jid']}"
112
- end
113
-
114
- def parse_params(params)
115
- score, jid = params.split("-")
116
- [score.to_f, jid]
117
- end
118
-
119
- def truncate(text, truncate_after_chars = 2000)
120
- truncate_after_chars && text.size > truncate_after_chars ? "#{text[0..truncate_after_chars]}..." : text
121
- end
122
-
123
- def display_args(args, truncate_after_chars = 2000)
124
- args.map do |arg|
125
- a = arg.inspect
126
- truncate(a)
127
- end.join(", ")
128
- end
129
-
130
- RETRY_JOB_KEYS = Set.new(%w(
131
- queue class args retry_count retried_at failed_at
132
- jid error_message error_class backtrace
133
- error_backtrace enqueued_at retry
134
- ))
135
-
136
- def retry_extra_items(retry_job)
137
- @retry_extra_items ||= {}.tap do |extra|
138
- retry_job.item.each do |key, value|
139
- extra[key] = value unless RETRY_JOB_KEYS.include?(key)
140
- end
141
- end
142
- end
21
+ DEFAULT_TABS = {
22
+ "Dashboard" => '',
23
+ "Workers" => 'workers',
24
+ "Queues" => 'queues',
25
+ "Retries" => 'retries',
26
+ "Scheduled" => 'scheduled',
27
+ }
143
28
 
144
- def tabs
145
- @tabs ||= {
146
- "Dashboard" => '',
147
- "Workers" => 'workers',
148
- "Queues" => 'queues',
149
- "Retries" => 'retries',
150
- "Scheduled" => 'scheduled',
151
- }
29
+ class << self
30
+ def default_tabs
31
+ DEFAULT_TABS
152
32
  end
153
33
 
154
34
  def custom_tabs
155
- self.class.tabs
156
- end
157
-
158
- def number_with_delimiter(number)
159
- begin
160
- Float(number)
161
- rescue ArgumentError, TypeError
162
- return number
163
- end
164
-
165
- options = {:delimiter => ',', :separator => '.'}
166
- parts = number.to_s.to_str.split('.')
167
- parts[0].gsub!(/(\d)(?=(\d\d\d)+(?!\d))/, "\\1#{options[:delimiter]}")
168
- parts.join(options[:separator])
169
- end
170
-
171
- def redis_keys
172
- ["redis_stats", "uptime_in_days", "connected_clients", "used_memory_human", "used_memory_peak_human"]
173
- end
174
-
175
- def h(text)
176
- ERB::Util.h(text)
35
+ @custom_tabs ||= {}
177
36
  end
37
+ alias_method :tabs, :custom_tabs
178
38
  end
179
39
 
180
40
  get "/workers" do
@@ -207,7 +67,7 @@ module Sidekiq
207
67
 
208
68
  post "/queues/:name/delete" do
209
69
  Sidekiq::Job.new(params[:key_val], params[:name]).delete
210
- redirect "#{root_path}queues/#{params[:name]}"
70
+ redirect_with_query("#{root_path}queues/#{params[:name]}")
211
71
  end
212
72
 
213
73
  get '/retries' do
@@ -236,7 +96,7 @@ module Sidekiq
236
96
  job.delete
237
97
  end
238
98
  end
239
- redirect "#{root_path}retries"
99
+ redirect_with_query("#{root_path}retries")
240
100
  end
241
101
 
242
102
  post "/retries/all/delete" do
@@ -259,7 +119,7 @@ module Sidekiq
259
119
  job.delete
260
120
  end
261
121
  end
262
- redirect "#{root_path}retries"
122
+ redirect_with_query("#{root_path}retries")
263
123
  end
264
124
 
265
125
  get '/scheduled' do
@@ -289,7 +149,7 @@ module Sidekiq
289
149
  end
290
150
  end
291
151
  end
292
- redirect "#{root_path}scheduled"
152
+ redirect_with_query("#{root_path}scheduled")
293
153
  end
294
154
 
295
155
  post "/scheduled/:key" do
@@ -302,20 +162,23 @@ module Sidekiq
302
162
  job.delete
303
163
  end
304
164
  end
305
- redirect "#{root_path}scheduled"
165
+ redirect_with_query("#{root_path}scheduled")
306
166
  end
307
167
 
308
168
  get '/' do
309
- @redis_info = Sidekiq.redis { |conn| conn.info }.select{ |k, v| redis_keys.include? k }
169
+ @redis_info = Sidekiq.redis { |conn| conn.info }.select{ |k, v| REDIS_KEYS.include? k }
310
170
  stats_history = Sidekiq::Stats::History.new((params[:days] || 30).to_i)
311
171
  @processed_history = stats_history.processed
312
172
  @failed_history = stats_history.failed
313
173
  erb :dashboard
314
174
  end
315
175
 
176
+ REDIS_KEYS = %w(redis_stats uptime_in_days connected_clients used_memory_human used_memory_peak_human)
177
+
316
178
  get '/dashboard/stats' do
317
179
  sidekiq_stats = Sidekiq::Stats.new
318
- redis_stats = Sidekiq.redis { |conn| conn.info }.select{ |k, v| redis_keys.include? k }
180
+ queue = Sidekiq::Queue.new
181
+ redis_stats = Sidekiq.redis { |conn| conn.info }.select{ |k, v| REDIS_KEYS.include? k }
319
182
 
320
183
  content_type :json
321
184
  Sidekiq.dump_json({
@@ -326,15 +189,11 @@ module Sidekiq
326
189
  enqueued: sidekiq_stats.enqueued,
327
190
  scheduled: sidekiq_stats.scheduled_size,
328
191
  retries: sidekiq_stats.retry_size,
192
+ default_latency: queue.latency,
329
193
  },
330
194
  redis: redis_stats
331
195
  })
332
196
  end
333
197
 
334
- def self.tabs
335
- @custom_tabs ||= {}
336
- end
337
-
338
198
  end
339
-
340
199
  end
@@ -0,0 +1,158 @@
1
+ require 'uri'
2
+
3
+ module Sidekiq
4
+ # This is not a public API
5
+ module WebHelpers
6
+ def strings
7
+ @@strings ||= begin
8
+ Dir["#{settings.locales}/*.yml"].inject({}) do |memo, file|
9
+ memo.merge(YAML.load(File.open(file)))
10
+ end
11
+ end
12
+ end
13
+
14
+ def locale
15
+ lang = (request.env["HTTP_ACCEPT_LANGUAGE"] || 'en')[0,2]
16
+ strings[lang] ? lang : 'en'
17
+ end
18
+
19
+ def get_locale
20
+ strings[locale]
21
+ end
22
+
23
+ def t(msg, options={})
24
+ string = get_locale[msg] || msg
25
+ string % options
26
+ end
27
+
28
+ def reset_worker_list
29
+ Sidekiq.redis do |conn|
30
+ workers = conn.smembers('workers')
31
+ conn.srem('workers', workers) if !workers.empty?
32
+ end
33
+ end
34
+
35
+ def workers_size
36
+ @workers_size ||= Sidekiq.redis do |conn|
37
+ conn.scard('workers')
38
+ end
39
+ end
40
+
41
+ def workers
42
+ @workers ||= begin
43
+ to_rem = []
44
+ workers = Sidekiq.redis do |conn|
45
+ conn.smembers('workers').map do |w|
46
+ msg = conn.get("worker:#{w}")
47
+ msg ? [w, Sidekiq.load_json(msg)] : (to_rem << w; nil)
48
+ end.compact.sort { |x| x[1] ? -1 : 1 }
49
+ end
50
+
51
+ # Detect and clear out any orphaned worker records.
52
+ # These can be left in Redis if Sidekiq crashes hard
53
+ # while processing jobs.
54
+ if to_rem.size > 0
55
+ Sidekiq.redis { |conn| conn.srem('workers', to_rem) }
56
+ end
57
+ workers
58
+ end
59
+ end
60
+
61
+ def stats
62
+ @stats ||= Sidekiq::Stats.new
63
+ end
64
+
65
+ def retries_with_score(score)
66
+ Sidekiq.redis do |conn|
67
+ conn.zrangebyscore('retry', score, score)
68
+ end.map { |msg| Sidekiq.load_json(msg) }
69
+ end
70
+
71
+ def location
72
+ Sidekiq.redis { |conn| conn.client.location }
73
+ end
74
+
75
+ def namespace
76
+ @@ns ||= Sidekiq.redis {|conn| conn.respond_to?(:namespace) ? conn.namespace : nil }
77
+ end
78
+
79
+ def root_path
80
+ "#{env['SCRIPT_NAME']}/"
81
+ end
82
+
83
+ def current_path
84
+ @current_path ||= request.path_info.gsub(/^\//,'')
85
+ end
86
+
87
+ def current_status
88
+ workers_size == 0 ? 'idle' : 'active'
89
+ end
90
+
91
+ def relative_time(time)
92
+ %{<time datetime="#{time.getutc.iso8601}">#{time}</time>}
93
+ end
94
+
95
+ def job_params(job, score)
96
+ "#{score}-#{job['jid']}"
97
+ end
98
+
99
+ def parse_params(params)
100
+ score, jid = params.split("-")
101
+ [score.to_f, jid]
102
+ end
103
+
104
+ def truncate(text, truncate_after_chars = 2000)
105
+ truncate_after_chars && text.size > truncate_after_chars ? "#{text[0..truncate_after_chars]}..." : text
106
+ end
107
+
108
+ def display_args(args, truncate_after_chars = 2000)
109
+ args.map do |arg|
110
+ a = arg.inspect
111
+ truncate(a)
112
+ end.join(", ")
113
+ end
114
+
115
+ RETRY_JOB_KEYS = Set.new(%w(
116
+ queue class args retry_count retried_at failed_at
117
+ jid error_message error_class backtrace
118
+ error_backtrace enqueued_at retry
119
+ ))
120
+
121
+ def retry_extra_items(retry_job)
122
+ @retry_extra_items ||= {}.tap do |extra|
123
+ retry_job.item.each do |key, value|
124
+ extra[key] = value unless RETRY_JOB_KEYS.include?(key)
125
+ end
126
+ end
127
+ end
128
+
129
+ def number_with_delimiter(number)
130
+ begin
131
+ Float(number)
132
+ rescue ArgumentError, TypeError
133
+ return number
134
+ end
135
+
136
+ options = {:delimiter => ',', :separator => '.'}
137
+ parts = number.to_s.to_str.split('.')
138
+ parts[0].gsub!(/(\d)(?=(\d\d\d)+(?!\d))/, "\\1#{options[:delimiter]}")
139
+ parts.join(options[:separator])
140
+ end
141
+
142
+ def h(text)
143
+ ERB::Util.h(text)
144
+ end
145
+
146
+ # Any paginated list that performs an action needs to redirect
147
+ # back to the proper page after performing that action.
148
+ def redirect_with_query(url)
149
+ r = request.referer
150
+ if r && r =~ /\?/
151
+ ref = URI(r)
152
+ redirect("#{url}?#{ref.query}")
153
+ else
154
+ redirect url
155
+ end
156
+ end
157
+ end
158
+ end
@@ -17,7 +17,7 @@ Gem::Specification.new do |gem|
17
17
  gem.add_dependency 'redis', '>= 3.0.4'
18
18
  gem.add_dependency 'redis-namespace', '>= 1.3.1'
19
19
  gem.add_dependency 'connection_pool', '>= 1.0.0'
20
- gem.add_dependency 'celluloid', '>= 0.15.1'
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'
@@ -1,6 +1,5 @@
1
1
  ---
2
2
  :verbose: false
3
- :environment: xzibit
4
3
  :require: ./test/fake_env.rb
5
4
  :pidfile: /tmp/sidekiq-config-test.pid
6
5
  :logfile: /tmp/sidekiq.log
@@ -20,7 +20,8 @@ end
20
20
  require 'minitest/autorun'
21
21
  require 'minitest/pride'
22
22
 
23
- require 'celluloid/autostart'
23
+ require 'celluloid/test'
24
+ Celluloid.boot
24
25
  require 'sidekiq'
25
26
  require 'sidekiq/util'
26
27
  Sidekiq.logger.level = Logger::ERROR
@@ -34,9 +35,3 @@ REDIS = Sidekiq::RedisConnection.create(:url => redis_url, :namespace => 'testy'
34
35
  Sidekiq.configure_client do |config|
35
36
  config.redis = { :url => redis_url, :namespace => 'testy' }
36
37
  end
37
-
38
- Celluloid.class_eval do
39
- def self.shutdown
40
- $stderr.puts "Celluloid shutdown disabled"
41
- end
42
- end
@@ -192,6 +192,26 @@ class TestApi < Sidekiq::Test
192
192
  assert_in_delta job.latency, 0.0, 0.01
193
193
  end
194
194
 
195
+ it 'can remove jobs when iterating over a sorted set' do
196
+ # scheduled jobs must be greater than SortedSet#each underlying page size
197
+ 51.times do
198
+ ApiWorker.perform_in(100, 'aaron')
199
+ end
200
+ set = Sidekiq::ScheduledSet.new
201
+ set.map(&:delete)
202
+ assert_equal set.size, 0
203
+ end
204
+
205
+ it 'can remove jobs when iterating over a queue' do
206
+ # initial queue size must be greater than Queue#each underlying page size
207
+ 51.times do
208
+ ApiWorker.perform_async(1, 'aaron')
209
+ end
210
+ q = Sidekiq::Queue.new
211
+ q.map(&:delete)
212
+ assert_equal q.size, 0
213
+ end
214
+
195
215
  it 'can find job by id in queues' do
196
216
  q = Sidekiq::Queue.new
197
217
  job_id = ApiWorker.perform_async(1, 'jason')
@@ -174,8 +174,8 @@ class TestCli < Sidekiq::Test
174
174
  assert_equal './test/fake_env.rb', Sidekiq.options[:require]
175
175
  end
176
176
 
177
- it 'sets environment' do
178
- assert_equal 'xzibit', Sidekiq.options[:environment]
177
+ it 'does not set environment' do
178
+ assert_equal nil, Sidekiq.options[:environment]
179
179
  end
180
180
 
181
181
  it 'sets concurrency' do
@@ -37,6 +37,20 @@ class TestExceptionHandler < Sidekiq::Test
37
37
  assert_match /Something didn't work!/, log[1], "didn't include the exception message"
38
38
  assert_match /test\/test_exception_handler.rb/, log[2], "didn't include the backtrace"
39
39
  end
40
+
41
+ describe "when the exception does not have a backtrace" do
42
+ it "does not fail" do
43
+ exception = ExceptionHandlerTestException.new
44
+ assert_nil exception.backtrace
45
+
46
+ begin
47
+ Component.new.handle_exception exception
48
+ pass
49
+ rescue => e
50
+ flunk "failed handling a nil backtrace"
51
+ end
52
+ end
53
+ end
40
54
  end
41
55
 
42
56
  describe "with fake Airbrake" do
@@ -324,6 +324,10 @@ class TestWeb < Sidekiq::Test
324
324
  it 'reports scheduled' do
325
325
  assert_equal 3, @response["sidekiq"]["scheduled"]
326
326
  end
327
+
328
+ it 'reports latency' do
329
+ assert_equal 0, @response["sidekiq"]["default_latency"]
330
+ end
327
331
  end
328
332
 
329
333
  describe "for redis" do
@@ -5,7 +5,7 @@
5
5
  <%= erb :_status %>
6
6
  </a>
7
7
  <ul class="nav navbar-nav">
8
- <% tabs.each do |title, url| %>
8
+ <% Sidekiq::Web.default_tabs.each do |title, url| %>
9
9
  <% if url == '' %>
10
10
  <li class="<%= current_path == url ? 'active' : '' %>">
11
11
  <a href="<%= root_path %><%= url %>"><%= t(title) %></a>
@@ -16,14 +16,11 @@
16
16
  </li>
17
17
  <% end %>
18
18
  <% end %>
19
- <% custom_tabs.each do |title, url| %>
19
+ <% Sidekiq::Web.custom_tabs.each do |title, url| %>
20
20
  <li class="<%= current_path.start_with?(url) ? 'active' : '' %>">
21
21
  <a href="<%= root_path %><%= url %>"><%= t(title) %></a>
22
22
  </li>
23
23
  <% end %>
24
-
25
-
26
-
27
24
  </ul>
28
25
 
29
26
  <div class="col-sm-2 pull-right poll-wrapper">
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.15.1
4
+ version: 2.15.2
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-10-04 00:00:00.000000000 Z
12
+ date: 2013-10-17 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.15.1
69
+ version: 0.15.2
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.15.1
77
+ version: 0.15.2
78
78
  - !ruby/object:Gem::Dependency
79
79
  name: json
80
80
  requirement: !ruby/object:Gem::Requirement
@@ -240,6 +240,7 @@ files:
240
240
  - lib/sidekiq/util.rb
241
241
  - lib/sidekiq/version.rb
242
242
  - lib/sidekiq/web.rb
243
+ - lib/sidekiq/web_helpers.rb
243
244
  - lib/sidekiq/worker.rb
244
245
  - lib/sidekiq/yaml_patch.rb
245
246
  - sidekiq.gemspec