sidekiq 2.5.4 → 2.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.

@@ -3,6 +3,7 @@ rvm:
3
3
  - 1.9.3
4
4
  - jruby-19mode
5
5
  - rbx-19mode
6
+ - 2.0.0
6
7
  branches:
7
8
  only:
8
9
  - master
@@ -15,3 +16,4 @@ matrix:
15
16
  allow_failures:
16
17
  - rvm: jruby-19mode
17
18
  - rvm: rbx-19mode
19
+ - rvm: 2.0.0
data/Changes.md CHANGED
@@ -1,3 +1,13 @@
1
+ 2.6.0
2
+ -----------
3
+
4
+ - Web UI much more mobile friendly now [brandonhilkert, #573]
5
+ - Enable live polling for every section in Web UI [brandonhilkert, #567]
6
+ - Add Stats API [brandonhilkert, #565]
7
+ - Add Stats::History API [brandonhilkert, #570]
8
+ - Add Dashboard to Web UI with live and historical stat graphs [brandonhilkert, #580]
9
+ - Add option to log output to a file, reopen log file on USR2 signal [mrnugget, #581]
10
+
1
11
  2.5.4
2
12
  -----------
3
13
 
@@ -19,8 +19,9 @@ class SinatraWorker
19
19
  end
20
20
 
21
21
  get '/' do
22
- @failed = Sidekiq.info[:failed]
23
- @processed = Sidekiq.info[:processed]
22
+ stats = Sidekiq::Stats.new
23
+ @failed = stats.failed
24
+ @processed = stats.processed
24
25
  @messages = $redis.lrange('sinkiq-example-messages', 0, -1)
25
26
  erb :index
26
27
  end
@@ -1,6 +1,91 @@
1
1
  require 'sidekiq'
2
2
 
3
3
  module Sidekiq
4
+ class Stats
5
+ def processed
6
+ count = Sidekiq.redis do |conn|
7
+ conn.get("stat:processed")
8
+ end
9
+ count.nil? ? 0 : count.to_i
10
+ end
11
+
12
+ def failed
13
+ count = Sidekiq.redis do |conn|
14
+ conn.get("stat:failed")
15
+ end
16
+ count.nil? ? 0 : count.to_i
17
+ end
18
+
19
+ def queues
20
+ Sidekiq.redis do |conn|
21
+ queues = conn.smembers('queues')
22
+
23
+ array_of_arrays = queues.inject({}) do |memo, queue|
24
+ memo[queue] = conn.llen("queue:#{queue}")
25
+ memo
26
+ end.sort_by { |_, size| size }
27
+
28
+ Hash[array_of_arrays.reverse]
29
+ end
30
+ end
31
+
32
+ def enqueued
33
+ queues.values.inject(&:+) || 0
34
+ end
35
+
36
+ class History
37
+ def initialize(days_previous, start_date = nil)
38
+ @days_previous = days_previous
39
+ @start_date = start_date || Time.now.utc.to_date
40
+ end
41
+
42
+ def processed
43
+ date_stat_hash("processed")
44
+ end
45
+
46
+ def failed
47
+ date_stat_hash("failed")
48
+ end
49
+
50
+ def self.cleanup
51
+ days_of_stats_to_keep = 180
52
+ today = Time.now.utc.to_date
53
+ delete_before_date = Time.now.utc.to_date - days_of_stats_to_keep
54
+
55
+ Sidekiq.redis do |conn|
56
+ processed_keys = conn.keys("stat:processed:*")
57
+ earliest = "stat:processed:#{delete_before_date.to_s}"
58
+ pkeys = processed_keys.select { |key| key < earliest }
59
+ conn.del(pkeys) if pkeys.size > 0
60
+
61
+ failed_keys = conn.keys("stat:failed:*")
62
+ earliest = "stat:failed:#{delete_before_date.to_s}"
63
+ fkeys = failed_keys.select { |key| key < earliest }
64
+ conn.del(fkeys) if fkeys.size > 0
65
+ end
66
+ end
67
+
68
+ private
69
+
70
+ def date_stat_hash(stat)
71
+ i = 0
72
+ stat_hash = {}
73
+
74
+ Sidekiq.redis do |conn|
75
+ while i < @days_previous
76
+ date = @start_date - i
77
+ value = conn.get("stat:#{stat}:#{date}")
78
+
79
+ stat_hash[date.to_s] = value ? value.to_i : 0
80
+
81
+ i += 1
82
+ end
83
+ end
84
+
85
+ stat_hash
86
+ end
87
+ end
88
+ end
4
89
 
5
90
  ##
6
91
  # Encapsulates a queue within Sidekiq.
@@ -110,11 +195,24 @@ module Sidekiq
110
195
  end
111
196
 
112
197
  def at
113
- Time.at(@score)
198
+ Time.at(score)
114
199
  end
115
200
 
116
201
  def delete
117
- @parent.delete(@score)
202
+ @parent.delete(score, jid)
203
+ end
204
+
205
+ def retry
206
+ raise "Retry not available on jobs not in the Retry queue." unless item["failed_at"]
207
+ Sidekiq.redis do |conn|
208
+ results = conn.zrangebyscore('retry', score, score)
209
+ conn.zremrangebyscore('retry', score, score)
210
+ results.map do |message|
211
+ msg = Sidekiq.load_json(message)
212
+ msg['retry_count'] = msg['retry_count'] - 1
213
+ conn.rpush("queue:#{msg['queue']}", Sidekiq.dump_json(msg))
214
+ end
215
+ end
118
216
  end
119
217
  end
120
218
 
@@ -146,11 +244,42 @@ module Sidekiq
146
244
  end
147
245
  end
148
246
 
149
- def delete(score)
150
- count = Sidekiq.redis do |conn|
151
- conn.zremrangebyscore(@zset, score, score)
247
+ def fetch(score, jid = nil)
248
+ elements = Sidekiq.redis do |conn|
249
+ conn.zrangebyscore(@zset, score, score)
250
+ end
251
+
252
+ elements.inject([]) do |result, element|
253
+ entry = SortedEntry.new(self, score, element)
254
+ if jid
255
+ result << entry if entry.jid == jid
256
+ else
257
+ result << entry
258
+ end
259
+ result
260
+ end
261
+ end
262
+
263
+ def delete(score, jid = nil)
264
+ if jid
265
+ elements = Sidekiq.redis do |conn|
266
+ conn.zrangebyscore(@zset, score, score)
267
+ end
268
+
269
+ elements_with_jid = elements.map do |element|
270
+ message = Sidekiq.load_json(element)
271
+
272
+ if message["jid"] == jid
273
+ Sidekiq.redis { |conn| conn.zrem(@zset, element) }
274
+ end
275
+ end
276
+ elements_with_jid.count != 0
277
+ else
278
+ count = Sidekiq.redis do |conn|
279
+ conn.zremrangebyscore(@zset, score, score)
280
+ end
281
+ count != 0
152
282
  end
153
- count != 0
154
283
  end
155
284
 
156
285
  def clear
@@ -4,6 +4,8 @@ Capistrano::Configuration.instance.load do
4
4
  after "deploy:start", "sidekiq:start"
5
5
  after "deploy:restart", "sidekiq:restart"
6
6
 
7
+ _cset(:sidekiq_cmd) { "#{fetch(:bundle_cmd, "bundle")} exec sidekiq" }
8
+ _cset(:sidekiqctl_cmd) { "#{fetch(:bundle_cmd, "bundle")} exec sidekiqctl" }
7
9
  _cset(:sidekiq_timeout) { 10 }
8
10
  _cset(:sidekiq_role) { :app }
9
11
  _cset(:sidekiq_pid) { "#{current_path}/tmp/pids/sidekiq.pid" }
@@ -19,14 +21,14 @@ Capistrano::Configuration.instance.load do
19
21
  desc "Quiet sidekiq (stop accepting new work)"
20
22
  task :quiet, :roles => lambda { fetch(:sidekiq_role) }, :on_no_matching_servers => :continue do
21
23
  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"
24
+ run "if [ -d #{current_path} ] && [ -f #{pid_file} ]; then cd #{current_path} && #{fetch(:sidekiqctl_cmd)} quiet #{pid_file} ; fi"
23
25
  end
24
26
  end
25
27
 
26
28
  desc "Stop sidekiq"
27
29
  task :stop, :roles => lambda { fetch(:sidekiq_role) }, :on_no_matching_servers => :continue do
28
30
  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"
31
+ run "if [ -d #{current_path} ] && [ -f #{pid_file} ]; then cd #{current_path} && #{fetch(:sidekiqctl_cmd)} stop #{pid_file} #{fetch :sidekiq_timeout} ; fi"
30
32
  end
31
33
  end
32
34
 
@@ -34,7 +36,7 @@ Capistrano::Configuration.instance.load do
34
36
  task :start, :roles => lambda { fetch(:sidekiq_role) }, :on_no_matching_servers => :continue do
35
37
  rails_env = fetch(:rails_env, "production")
36
38
  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
39
+ run "cd #{current_path} ; nohup #{fetch(:sidekiq_cmd)} -e #{rails_env} -C #{current_path}/config/sidekiq.yml -P #{pid_file} >> #{current_path}/log/sidekiq.log 2>&1 &", :pty => false
38
40
  end
39
41
  end
40
42
 
@@ -15,6 +15,13 @@ trap 'USR1' do
15
15
  mgr.async.stop if mgr
16
16
  end
17
17
 
18
+ trap 'USR2' do
19
+ if Sidekiq.options[:logfile]
20
+ Sidekiq.logger.info "Received USR2, reopening log file"
21
+ Sidekiq::Logging.initialize_logger(Sidekiq.options[:logfile])
22
+ end
23
+ end
24
+
18
25
  trap 'TTIN' do
19
26
  Thread.list.each do |thread|
20
27
  Sidekiq.logger.info "Thread TID-#{thread.object_id.to_s(36)} #{thread['label']}"
@@ -56,15 +63,12 @@ module Sidekiq
56
63
 
57
64
  def parse(args=ARGV)
58
65
  @code = nil
59
- Sidekiq.logger
60
66
 
61
67
  cli = parse_options(args)
62
68
  config = parse_config(cli)
63
69
  options.merge!(config.merge(cli))
64
70
 
65
- Sidekiq.logger.level = Logger::DEBUG if options[:verbose]
66
- Celluloid.logger = nil unless options[:verbose]
67
-
71
+ initialize_logger
68
72
  validate!
69
73
  write_pid
70
74
  boot_system
@@ -75,6 +79,8 @@ module Sidekiq
75
79
  logger.info "Running in #{RUBY_DESCRIPTION}"
76
80
  logger.info Sidekiq::LICENSE
77
81
 
82
+ Sidekiq::Stats::History.cleanup
83
+
78
84
  @manager = Sidekiq::Manager.new(options)
79
85
  poller = Sidekiq::Scheduled::Poller.new
80
86
  begin
@@ -166,8 +172,8 @@ module Sidekiq
166
172
  parse_queues opts, queues_and_weights
167
173
  end
168
174
 
169
- o.on "-v", "--verbose", "Print more verbose output" do
170
- Sidekiq.logger.level = ::Logger::DEBUG
175
+ o.on "-v", "--verbose", "Print more verbose output" do |arg|
176
+ opts[:verbose] = arg
171
177
  end
172
178
 
173
179
  o.on '-e', '--environment ENV', "Application environment" do |arg|
@@ -198,6 +204,10 @@ module Sidekiq
198
204
  opts[:config_file] = arg
199
205
  end
200
206
 
207
+ o.on '-L', '--logfile PATH', "path to writable logfile" do |arg|
208
+ opts[:logfile] = arg
209
+ end
210
+
201
211
  o.on '-V', '--version', "Print version and exit" do |arg|
202
212
  puts "Sidekiq #{Sidekiq::VERSION}"
203
213
  die(0)
@@ -213,6 +223,13 @@ module Sidekiq
213
223
  opts
214
224
  end
215
225
 
226
+ def initialize_logger
227
+ Sidekiq::Logging.initialize_logger(options[:logfile]) if options[:logfile]
228
+
229
+ Sidekiq.logger.level = Logger::DEBUG if options[:verbose]
230
+ Celluloid.logger = nil unless options[:verbose]
231
+ end
232
+
216
233
  def write_pid
217
234
  if path = options[:pidfile]
218
235
  File.open(path, 'w') do |f|
@@ -25,13 +25,15 @@ module Sidekiq
25
25
  end
26
26
  end
27
27
 
28
+ def self.initialize_logger(log_target = STDOUT)
29
+ @logger = Logger.new(log_target)
30
+ @logger.level = Logger::INFO
31
+ @logger.formatter = Pretty.new
32
+ @logger
33
+ end
34
+
28
35
  def self.logger
29
- @logger ||= begin
30
- log = Logger.new(STDOUT)
31
- log.level = Logger::INFO
32
- log.formatter = Pretty.new
33
- log
34
- end
36
+ @logger || initialize_logger
35
37
  end
36
38
 
37
39
  def self.logger=(log)
@@ -41,6 +43,5 @@ module Sidekiq
41
43
  def logger
42
44
  Sidekiq::Logging.logger
43
45
  end
44
-
45
46
  end
46
47
  end
@@ -80,6 +80,7 @@ module Sidekiq
80
80
  redis do |conn|
81
81
  conn.multi do
82
82
  conn.incrby("stat:failed", 1)
83
+ conn.incrby("stat:failed:#{Time.now.utc.to_date}", 1)
83
84
  end
84
85
  end
85
86
  raise
@@ -90,6 +91,7 @@ module Sidekiq
90
91
  conn.del("worker:#{self}")
91
92
  conn.del("worker:#{self}:started")
92
93
  conn.incrby("stat:processed", 1)
94
+ conn.incrby("stat:processed:#{Time.now.utc.to_date}", 1)
93
95
  end
94
96
  end
95
97
  end
@@ -1,31 +1,8 @@
1
1
  module Sidekiq
2
2
  module_function
3
3
 
4
- def info
5
- results = {}
6
- processed, failed, queues = Sidekiq.redis { |conn|
7
- conn.multi do
8
- conn.get('stat:processed')
9
- conn.get('stat:failed')
10
- conn.smembers('queues')
11
- end
12
- }
13
- results[:queues_with_sizes] = Sidekiq.redis do |conn|
14
- queues.inject({}) { |memo, q|
15
- memo[q] = conn.llen("queue:#{q}")
16
- memo
17
- }.sort_by { |_, size| size }
18
- end
19
- results[:processed] = (processed || 0).to_i
20
- results[:failed] = (failed || 0).to_i
21
- results[:backlog] = results[:queues_with_sizes].
22
- map {|_, size| size }.
23
- inject(0) {|memo, val| memo + val }
24
- results
25
- end
26
-
27
4
  def size(*queues)
28
- return info[:backlog] if queues.empty?
5
+ return Sidekiq::Stats.new.enqueued if queues.empty?
29
6
 
30
7
  Sidekiq.redis { |conn|
31
8
  conn.multi {
@@ -1,3 +1,3 @@
1
1
  module Sidekiq
2
- VERSION = "2.5.4"
2
+ VERSION = "2.6.0"
3
3
  end
@@ -34,28 +34,16 @@ module Sidekiq
34
34
  end
35
35
  end
36
36
 
37
- def info
38
- @info ||= Sidekiq.info
37
+ def stats
38
+ @stats ||= Sidekiq::Stats.new
39
39
  end
40
40
 
41
- def processed
42
- info[:processed]
41
+ def scheduled_job_count
42
+ Sidekiq::ScheduledSet.new.size
43
43
  end
44
44
 
45
- def failed
46
- info[:failed]
47
- end
48
-
49
- def zcard(name)
50
- Sidekiq.redis { |conn| conn.zcard(name) }
51
- end
52
-
53
- def queues
54
- @queues ||= Sidekiq.info[:queues_with_sizes]
55
- end
56
-
57
- def backlog
58
- info[:backlog]
45
+ def retry_job_count
46
+ Sidekiq::RetrySet.new.size
59
47
  end
60
48
 
61
49
  def retries_with_score(score)
@@ -86,6 +74,15 @@ module Sidekiq
86
74
  %{<time datetime="#{time.getutc.iso8601}">#{time}</time>}
87
75
  end
88
76
 
77
+ def job_params(job, score)
78
+ "#{score}-#{job['jid']}"
79
+ end
80
+
81
+ def parse_params(params)
82
+ score, jid = params.split("-")
83
+ [score.to_f, jid]
84
+ end
85
+
89
86
  def display_args(args, count=100)
90
87
  args.map { |arg| a = arg.inspect; a.size > count ? "#{a[0..count]}..." : a }.join(", ")
91
88
  end
@@ -112,12 +109,8 @@ module Sidekiq
112
109
  slim :index
113
110
  end
114
111
 
115
- get "/poll" do
116
- slim :poll, layout: false
117
- end
118
-
119
112
  get "/queues" do
120
- @queues = queues
113
+ @queues = Sidekiq::Stats.new.queues
121
114
  slim :queues
122
115
  end
123
116
 
@@ -141,20 +134,10 @@ module Sidekiq
141
134
  end
142
135
 
143
136
  post "/queues/:name/delete" do
144
- Sidekiq.redis do |conn|
145
- conn.lrem("queue:#{params[:name]}", 0, params[:key_val])
146
- end
137
+ Sidekiq::Job.new(params[:key_val], params[:name]).delete
147
138
  redirect "#{root_path}queues/#{params[:name]}"
148
139
  end
149
140
 
150
- get "/retries/:score" do
151
- halt 404 unless params[:score]
152
- @score = params[:score].to_f
153
- @retries = retries_with_score(@score)
154
- redirect "#{root_path}retries" if @retries.empty?
155
- slim :retry
156
- end
157
-
158
141
  get '/retries' do
159
142
  @count = (params[:count] || 25).to_i
160
143
  (@current_page, @total_size, @retries) = page("retry", params[:page], @count)
@@ -162,31 +145,22 @@ module Sidekiq
162
145
  slim :retries
163
146
  end
164
147
 
165
- get '/scheduled' do
166
- @count = (params[:count] || 25).to_i
167
- (@current_page, @total_size, @scheduled) = page("schedule", params[:page], @count)
168
- @scheduled = @scheduled.map {|msg, score| [Sidekiq.load_json(msg), score] }
169
- slim :scheduled
170
- end
171
-
172
- post '/scheduled' do
173
- halt 404 unless params[:score]
174
- halt 404 unless params['delete']
175
- params[:score].each do |score|
176
- s = score.to_f
177
- process_score('schedule', s, :delete)
178
- end
179
- redirect "#{root_path}scheduled"
148
+ get "/retries/:key" do
149
+ halt 404 unless params['key']
150
+ @retry = Sidekiq::RetrySet.new.fetch(*parse_params(params['key'])).first
151
+ redirect "#{root_path}retries" if @retry.nil?
152
+ slim :retry
180
153
  end
181
154
 
182
155
  post '/retries' do
183
- halt 404 unless params[:score]
184
- params[:score].each do |score|
185
- s = score.to_f
156
+ halt 404 unless params['key']
157
+
158
+ params['key'].each do |key|
159
+ job = Sidekiq::RetrySet.new.fetch(*parse_params(key)).first
186
160
  if params['retry']
187
- process_score('retry', s, :retry)
161
+ job.retry
188
162
  elsif params['delete']
189
- process_score('retry', s, :delete)
163
+ job.delete
190
164
  end
191
165
  end
192
166
  redirect "#{root_path}retries"
@@ -198,40 +172,49 @@ module Sidekiq
198
172
  end
199
173
 
200
174
  post "/retries/all/retry" do
201
- Sidekiq::RetrySet.new.each do |job|
202
- process_score('retry', job.score, :retry)
203
- end
175
+ Sidekiq::RetrySet.new.each { |job| job.retry }
204
176
  redirect "#{root_path}retries"
205
177
  end
206
178
 
207
- post "/retries/:score" do
208
- halt 404 unless params[:score]
209
- score = params[:score].to_f
179
+ post "/retries/:key" do
180
+ halt 404 unless params['key']
181
+ job = Sidekiq::RetrySet.new.fetch(*parse_params(params['key'])).first
210
182
  if params['retry']
211
- process_score('retry', score, :retry)
183
+ job.retry
212
184
  elsif params['delete']
213
- process_score('retry', score, :delete)
185
+ job.delete
214
186
  end
215
187
  redirect "#{root_path}retries"
216
188
  end
217
189
 
218
- def process_score(set, score, operation)
219
- case operation
220
- when :retry
221
- Sidekiq.redis do |conn|
222
- results = conn.zrangebyscore(set, score, score)
223
- conn.zremrangebyscore(set, score, score)
224
- results.map do |message|
225
- msg = Sidekiq.load_json(message)
226
- msg['retry_count'] = msg['retry_count'] - 1
227
- conn.rpush("queue:#{msg['queue']}", Sidekiq.dump_json(msg))
228
- end
229
- end
230
- when :delete
231
- Sidekiq.redis do |conn|
232
- conn.zremrangebyscore(set, score, score)
233
- end
190
+ get '/scheduled' do
191
+ @count = (params[:count] || 25).to_i
192
+ (@current_page, @total_size, @scheduled) = page("schedule", params[:page], @count)
193
+ @scheduled = @scheduled.map {|msg, score| [Sidekiq.load_json(msg), score] }
194
+ slim :scheduled
195
+ end
196
+
197
+ post '/scheduled' do
198
+ halt 404 unless params['key']
199
+ halt 404 unless params['delete']
200
+ params['key'].each do |key|
201
+ Sidekiq::ScheduledSet.new.fetch(*parse_params(key)).first.delete
234
202
  end
203
+ redirect "#{root_path}scheduled"
204
+ end
205
+
206
+ get '/dashboard' do
207
+ @redis_info = Sidekiq.redis{ |conn| conn.info }
208
+ stats_history = Sidekiq::Stats::History.new((params[:days] || 30).to_i)
209
+ @processed_history = stats_history.processed
210
+ @failed_history = stats_history.failed
211
+ slim :dashboard
212
+ end
213
+
214
+ get '/dashboard/stats' do
215
+ stats = Sidekiq::Stats.new
216
+ content_type :json
217
+ Sidekiq.dump_json({ processed: stats.processed, failed: stats.failed })
235
218
  end
236
219
 
237
220
  def self.tabs
@@ -239,7 +222,8 @@ module Sidekiq
239
222
  "Workers" =>'',
240
223
  "Queues" =>'queues',
241
224
  "Retries" =>'retries',
242
- "Scheduled" =>'scheduled'
225
+ "Scheduled" =>'scheduled',
226
+ "Dashboard" =>'dashboard'
243
227
  }
244
228
  end
245
229