sidekiq_utils 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/Gemfile +16 -0
- data/Gemfile.lock +96 -0
- data/LICENSE +21 -0
- data/README.md +185 -0
- data/Rakefile +50 -0
- data/VERSION +1 -0
- data/lib/sidekiq_utils/additional_serialization.rb +56 -0
- data/lib/sidekiq_utils/deprioritize.rb +19 -0
- data/lib/sidekiq_utils/enqueued_jobs_helper.rb +45 -0
- data/lib/sidekiq_utils/find_optional.rb +24 -0
- data/lib/sidekiq_utils/job_counter.rb +161 -0
- data/lib/sidekiq_utils/latency_alert.rb +97 -0
- data/lib/sidekiq_utils/middleware/client/additional_serialization.rb +20 -0
- data/lib/sidekiq_utils/middleware/client/deprioritize.rb +14 -0
- data/lib/sidekiq_utils/middleware/client/job_counter.rb +17 -0
- data/lib/sidekiq_utils/middleware/server/additional_serialization.rb +20 -0
- data/lib/sidekiq_utils/middleware/server/find_optional.rb +20 -0
- data/lib/sidekiq_utils/middleware/server/job_counter.rb +15 -0
- data/lib/sidekiq_utils/middleware/server/memory_monitor.rb +46 -0
- data/lib/sidekiq_utils/middleware/server/throughput_monitor.rb +25 -0
- data/lib/sidekiq_utils/redis_monitor_storage.rb +81 -0
- data/lib/sidekiq_utils/views/job_counts.erb +34 -0
- data/lib/sidekiq_utils/views/memory.erb +33 -0
- data/lib/sidekiq_utils/views/throughput.erb +27 -0
- data/lib/sidekiq_utils/web_extensions/job_counter.rb +35 -0
- data/lib/sidekiq_utils/web_extensions/memory_monitor.rb +26 -0
- data/lib/sidekiq_utils/web_extensions/throughput_monitor.rb +32 -0
- data/lib/sidekiq_utils.rb +21 -0
- data/sidekiq_utils.gemspec +84 -0
- metadata +158 -0
@@ -0,0 +1,97 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
|
3
|
+
module SidekiqUtils
|
4
|
+
class LatencyAlert
|
5
|
+
REDIS_KEY = "sidekiq_queue_latency_alert"
|
6
|
+
|
7
|
+
class << self
|
8
|
+
def check!
|
9
|
+
alerts = {}
|
10
|
+
Sidekiq::Queue.all.each do |queue|
|
11
|
+
threshold = config['alert_thresholds'][queue.name]
|
12
|
+
next if threshold == :disabled
|
13
|
+
threshold ||= config['alert_thresholds']['default'] || 10.minutes
|
14
|
+
if (latency = queue.latency) > threshold
|
15
|
+
alerts[queue.name] = latency
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
if alerts.blank?
|
20
|
+
if should_alert_back_to_normal?
|
21
|
+
Sidekiq.redis { |r| r.del(REDIS_KEY) }
|
22
|
+
slack_alert("All queues under their thresholds.")
|
23
|
+
end
|
24
|
+
return false
|
25
|
+
end
|
26
|
+
|
27
|
+
alert_message = ["Sidekiq queue latency over threshold:"]
|
28
|
+
alerts.each do |queue, latency|
|
29
|
+
alert_message << "Queue #{queue} is #{formatted_latency(latency)} behind"
|
30
|
+
end
|
31
|
+
alert_message = alert_message.join("\n")
|
32
|
+
if should_alert_again?(alert_message)
|
33
|
+
slack_alert(alert_message)
|
34
|
+
end
|
35
|
+
|
36
|
+
true
|
37
|
+
end
|
38
|
+
|
39
|
+
def config
|
40
|
+
@config ||= (YAML.load(ERB.new(
|
41
|
+
File.read('config/sidekiq_utils.yml')).result) || {})
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
def formatted_latency(latency)
|
46
|
+
latency_days = (latency.to_f / 1.day).floor
|
47
|
+
if latency < 1.day
|
48
|
+
formatted_latency =
|
49
|
+
ActionController::Base.helpers.distance_of_time_in_words(latency)
|
50
|
+
else
|
51
|
+
formatted_latency = "#{latency_days} #{"day".pluralize(latency_days)}"
|
52
|
+
end
|
53
|
+
if latency > 1.day
|
54
|
+
latency_in_day = latency - latency_days * 1.day
|
55
|
+
if latency_in_day >= 45.minutes
|
56
|
+
formatted_latency += " and " +
|
57
|
+
ActionController::Base.helpers.
|
58
|
+
distance_of_time_in_words(latency_in_day)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
formatted_latency
|
62
|
+
end
|
63
|
+
|
64
|
+
def slack_alert(alert_message)
|
65
|
+
Array.wrap(config['channels_to_alert']).each do |slack_name|
|
66
|
+
Slack.send_message(
|
67
|
+
slack_name, alert_message,
|
68
|
+
icon: ':alarm_clock:', username: 'Sidekiq alerts')
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def should_alert_back_to_normal?
|
73
|
+
Sidekiq.redis do |redis|
|
74
|
+
redis.get(REDIS_KEY).present?
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def should_alert_again?(message)
|
79
|
+
Sidekiq.redis do |redis|
|
80
|
+
last_alert = redis.get(REDIS_KEY)
|
81
|
+
last_alert = JSON.parse(last_alert) if last_alert
|
82
|
+
if !last_alert ||
|
83
|
+
last_alert['message_hash'] != Digest::SHA1.hexdigest(message) ||
|
84
|
+
last_alert['time'] < (config['repeat_alert_every'] || 60).to_i.minutes.ago.to_i
|
85
|
+
redis.set(REDIS_KEY, {
|
86
|
+
'message_hash' => Digest::SHA1.hexdigest(message),
|
87
|
+
'time' => Time.now.to_i,
|
88
|
+
}.to_json)
|
89
|
+
true
|
90
|
+
else
|
91
|
+
false
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module SidekiqUtils
|
2
|
+
module Middleware
|
3
|
+
module Client
|
4
|
+
class AdditionalSerialization
|
5
|
+
def call(worker_class, job, queue, redis_pool)
|
6
|
+
if job['class'] == "ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper"
|
7
|
+
# this is handled in ActiveJob, as it would otherwise raise an
|
8
|
+
# exception before it even gets here
|
9
|
+
return yield
|
10
|
+
end
|
11
|
+
|
12
|
+
job['args'] = job['args'].map do |arg|
|
13
|
+
::SidekiqUtils::AdditionalSerialization.wrap_argument(arg)
|
14
|
+
end
|
15
|
+
yield
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module SidekiqUtils
|
2
|
+
module Middleware
|
3
|
+
module Client
|
4
|
+
class Deprioritize
|
5
|
+
def call(worker_class, job, queue, redis_pool)
|
6
|
+
if Thread.current[:deprioritize_worker_classes].to_a.include?(worker_class)
|
7
|
+
job['queue'] = 'low'
|
8
|
+
end
|
9
|
+
yield
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module SidekiqUtils
|
2
|
+
module Middleware
|
3
|
+
module Client
|
4
|
+
class JobCounter
|
5
|
+
def call(worker_class, job, queue, redis_pool)
|
6
|
+
unless job['at']
|
7
|
+
# don't count when jobs get put on the scheduled set, because
|
8
|
+
# otherwise we'll double-count them when they get popped and moved
|
9
|
+
# to a work queue.
|
10
|
+
SidekiqUtils::JobCounter.increment(job)
|
11
|
+
end
|
12
|
+
yield
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module SidekiqUtils
|
2
|
+
module Middleware
|
3
|
+
module Server
|
4
|
+
class AdditionalSerialization
|
5
|
+
def call(worker, job, queue)
|
6
|
+
if job['class'] == "ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper"
|
7
|
+
# this is handled in ActiveJob, as it would otherwise raise an
|
8
|
+
# exception before it even gets here
|
9
|
+
return yield
|
10
|
+
end
|
11
|
+
|
12
|
+
job['args'] = job['args'].map do |arg|
|
13
|
+
::SidekiqUtils::AdditionalSerialization.unwrap_argument(arg)
|
14
|
+
end
|
15
|
+
yield
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module SidekiqUtils
|
2
|
+
module Middleware
|
3
|
+
module Server
|
4
|
+
class FindOptional
|
5
|
+
def call(worker, job, queue)
|
6
|
+
begin
|
7
|
+
yield
|
8
|
+
rescue SidekiqUtils::FindOptional::NotFoundError
|
9
|
+
if queue == 'retry_once'
|
10
|
+
# do nothing; this is already the retry and it failed again
|
11
|
+
else
|
12
|
+
worker.class.set(queue: :retry_once).
|
13
|
+
perform_in(30.seconds, *job['args'])
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module SidekiqUtils
|
2
|
+
module Middleware
|
3
|
+
module Server
|
4
|
+
class JobCounter
|
5
|
+
def call(worker, job, queue)
|
6
|
+
# we decrement here whether the job succeeds or not, because
|
7
|
+
# re-enqueuing from the retry queue triggers the client middleware
|
8
|
+
# and thus another increment even in the case of an error
|
9
|
+
SidekiqUtils::JobCounter.decrement(job)
|
10
|
+
yield
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require 'objspace'
|
2
|
+
|
3
|
+
module SidekiqUtils
|
4
|
+
module Middleware
|
5
|
+
module Server
|
6
|
+
class MemoryMonitor
|
7
|
+
def call(worker, job, queue)
|
8
|
+
return yield unless Sidekiq.options[:concurrency] == 1
|
9
|
+
|
10
|
+
objects_before = count_allocated_objects
|
11
|
+
memory_before = get_allocated_memory
|
12
|
+
|
13
|
+
GC.start(full_mark: true)
|
14
|
+
GC.disable
|
15
|
+
begin
|
16
|
+
yield
|
17
|
+
ensure
|
18
|
+
GC.enable
|
19
|
+
GC.start(full_mark: true)
|
20
|
+
objects_after = count_allocated_objects
|
21
|
+
memory_after = get_allocated_memory
|
22
|
+
|
23
|
+
object_growth = objects_after - objects_before
|
24
|
+
SidekiqUtils::RedisMonitorStorage.store(
|
25
|
+
'sidekiq_memory', 'object', job, object_growth)
|
26
|
+
Sidekiq.logger.info("Object growth: #{object_growth}")
|
27
|
+
|
28
|
+
memory_growth = memory_after - memory_before
|
29
|
+
SidekiqUtils::RedisMonitorStorage.store(
|
30
|
+
'sidekiq_memory', 'memory', job, memory_growth)
|
31
|
+
Sidekiq.logger.info("Memory growth: #{memory_growth}")
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
def count_allocated_objects
|
37
|
+
ObjectSpace.each_object.inject(0) {|count, obj| count + 1 }
|
38
|
+
end
|
39
|
+
|
40
|
+
def get_allocated_memory
|
41
|
+
ObjectSpace.memsize_of_all
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module SidekiqUtils
|
2
|
+
module Middleware
|
3
|
+
module Server
|
4
|
+
class ThroughputMonitor
|
5
|
+
def call(worker, job, queue)
|
6
|
+
start = Time.now
|
7
|
+
begin
|
8
|
+
yield
|
9
|
+
ensure
|
10
|
+
elapsed = Time.now - start
|
11
|
+
elapsed_ms = (elapsed * 1_000).round
|
12
|
+
|
13
|
+
SidekiqUtils::RedisMonitorStorage.store(
|
14
|
+
'sidekiq_elapsed', 'elapsed', job, elapsed_ms)
|
15
|
+
Sidekiq.redis do |redis|
|
16
|
+
redis.hset('sidekiq_last_run',
|
17
|
+
SidekiqUtils::RedisMonitorStorage.job_prefix(job),
|
18
|
+
Time.now.to_i)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
module SidekiqUtils
|
2
|
+
module RedisMonitorStorage
|
3
|
+
class << self
|
4
|
+
def add_first_argument_to_job_key(*klasses)
|
5
|
+
@first_argument_to_job_key_for ||= []
|
6
|
+
@first_argument_to_job_key_for |= klasses
|
7
|
+
end
|
8
|
+
|
9
|
+
def store(key, prefix, job, value)
|
10
|
+
Sidekiq.redis do |redis|
|
11
|
+
redis.multi do
|
12
|
+
redis.hincrby(key, full_prefix(job, prefix, 'sum'), value)
|
13
|
+
redis.hincrby(key, full_prefix(job, prefix, 'count'), 1)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def retrieve(top_level_key, prefix)
|
19
|
+
data = {}
|
20
|
+
Sidekiq.redis {|r| r.hgetall(top_level_key) }.each do |key, value|
|
21
|
+
(job, prefix_type, date, value_type) = key.split('||')
|
22
|
+
next unless prefix_type == prefix
|
23
|
+
|
24
|
+
if Date.parse(date) < 1.week.ago
|
25
|
+
# expired data, get rid of it
|
26
|
+
Sidekiq.redis {|r| r.hdel(top_level_key, key) }
|
27
|
+
else
|
28
|
+
data[job] ||= { 'sum' => 0, 'count' => 0 }
|
29
|
+
data[job][value_type] += value.to_i
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
data.each do |job, values|
|
34
|
+
values['average'] = (values['sum'].to_f / values['count'].to_i).round
|
35
|
+
end
|
36
|
+
data
|
37
|
+
end
|
38
|
+
|
39
|
+
def full_prefix(job, prefix = nil, last_prefix = nil)
|
40
|
+
job_prefix = job_prefix(job)
|
41
|
+
full_prefix = [job_prefix, prefix, Date.today.to_s(:medium), last_prefix]
|
42
|
+
full_prefix.compact.join('||')
|
43
|
+
end
|
44
|
+
|
45
|
+
def job_prefix(job, unwrap_arguments: false)
|
46
|
+
arguments = arguments(job)
|
47
|
+
if unwrap_arguments
|
48
|
+
arguments = arguments.
|
49
|
+
map {|arg| SidekiqUtils::AdditionalSerialization.unwrap_argument(arg) }
|
50
|
+
end
|
51
|
+
|
52
|
+
if active_job?(job)
|
53
|
+
job_prefix = job['wrapped']
|
54
|
+
else
|
55
|
+
job_prefix = job['class']
|
56
|
+
end
|
57
|
+
|
58
|
+
case job_prefix
|
59
|
+
when 'ActionMailer::DeliveryJob'
|
60
|
+
job_prefix += "[#{arguments[0..1].join('#')}]"
|
61
|
+
when *(@first_argument_to_job_key_for.to_a)
|
62
|
+
job_prefix += "[#{arguments[0]}]"
|
63
|
+
end
|
64
|
+
|
65
|
+
job_prefix
|
66
|
+
end
|
67
|
+
|
68
|
+
def arguments(job)
|
69
|
+
if active_job?(job)
|
70
|
+
job['args'].first['arguments']
|
71
|
+
else
|
72
|
+
job['args']
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def active_job?(job)
|
77
|
+
job['class'] == "ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper"
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
<header class='row'>
|
2
|
+
<div class='col-sm-5'>
|
3
|
+
<h3>Enqueued Job Counts</h3>
|
4
|
+
</div>
|
5
|
+
</header>
|
6
|
+
|
7
|
+
<p>
|
8
|
+
This data may not be 100% accurate if jobs haven been manually deleted
|
9
|
+
or queues have been manually cleared.
|
10
|
+
</p>
|
11
|
+
<table class='table table-striped table-bordered table-white'>
|
12
|
+
<thead>
|
13
|
+
<th>Queue</th>
|
14
|
+
<th>Job</th>
|
15
|
+
<th>Count</th>
|
16
|
+
<th>Approx. sequential run time (days:hours:minutes)</th>
|
17
|
+
</thead>
|
18
|
+
<tbody>
|
19
|
+
<% @counts.each do |values| %>
|
20
|
+
<tr>
|
21
|
+
<td><%= values[:queue] %></td>
|
22
|
+
<td><%= values[:job] %></td>
|
23
|
+
<td><%= ActiveSupport::NumberHelper.number_to_delimited(values[:count]) %></td>
|
24
|
+
<td>
|
25
|
+
<% if values[:avg_runtime] %>
|
26
|
+
<%= values[:runtime_day] %>:<%= values[:runtime_hour].to_s.rjust(2, '0') %>:<%= values[:runtime_min].to_s.rjust(2, '0') %>
|
27
|
+
<% else %>
|
28
|
+
-/-
|
29
|
+
<% end %>
|
30
|
+
</td>
|
31
|
+
</tr>
|
32
|
+
<% end %>
|
33
|
+
</tbody>
|
34
|
+
</table>
|
@@ -0,0 +1,33 @@
|
|
1
|
+
<header class='row'>
|
2
|
+
<div class='col-sm-5'>
|
3
|
+
<h3>Memory</h3>
|
4
|
+
</div>
|
5
|
+
</header>
|
6
|
+
|
7
|
+
<p>
|
8
|
+
Data from the trailing seven days. Only a small percentage of jobs get
|
9
|
+
profiled. The "absolute" numbers refer only to those jobs that did get
|
10
|
+
profiled.
|
11
|
+
</p>
|
12
|
+
<table class='table table-striped table-bordered table-white'>
|
13
|
+
<thead>
|
14
|
+
<tr>
|
15
|
+
<th>Job</th>
|
16
|
+
<th>Memory growth (absolute)</th>
|
17
|
+
<th>Memory growth (per job)</th>
|
18
|
+
<th>Object growth (absolute)</th>
|
19
|
+
<th>Object growth (per job)</th>
|
20
|
+
</tr>
|
21
|
+
</thead>
|
22
|
+
<tbody>
|
23
|
+
<% @memory.each do |(job, avg_memory, avg_object, abs_memory, abs_object)| %>
|
24
|
+
<tr>
|
25
|
+
<td><%= job %></td>
|
26
|
+
<td><%= ActiveSupport::NumberHelper.number_to_human_size(abs_memory) %></td>
|
27
|
+
<td><%= ActiveSupport::NumberHelper.number_to_human_size(avg_memory) %></td>
|
28
|
+
<td><%= ActiveSupport::NumberHelper.number_to_delimited(abs_object) %></td>
|
29
|
+
<td><%= ActiveSupport::NumberHelper.number_to_delimited(avg_object) %></td>
|
30
|
+
</tr>
|
31
|
+
<% end %>
|
32
|
+
</tbody>
|
33
|
+
</table
|
@@ -0,0 +1,27 @@
|
|
1
|
+
<header class='row'>
|
2
|
+
<div class='col-sm-5'>
|
3
|
+
<h3>Throughput</h3>
|
4
|
+
</div>
|
5
|
+
</header>
|
6
|
+
|
7
|
+
<p>Data from the trailing seven days.</p>
|
8
|
+
<table class='table table-striped table-bordered table-white'>
|
9
|
+
<thead>
|
10
|
+
<tr>
|
11
|
+
<th>Job</th>
|
12
|
+
<th>Avg. execution time (minute:second.millisecond)</th>
|
13
|
+
<th>Total processed</th>
|
14
|
+
<th>Last run at</th>
|
15
|
+
</tr>
|
16
|
+
</thead>
|
17
|
+
<tbody>
|
18
|
+
<% @throughput.each do |(job, avg_execution_time_ms, total_processed, last_run_at)| %>
|
19
|
+
<tr>
|
20
|
+
<td><%= job %></td>
|
21
|
+
<td><%= Time.at(avg_execution_time_ms.to_f / 1_000).utc.strftime("%M:%S.%L") %></td>
|
22
|
+
<td><%= ActiveSupport::NumberHelper.number_to_delimited(total_processed) %></td>
|
23
|
+
<td><%= last_run_at %></td>
|
24
|
+
</tr>
|
25
|
+
<% end %>
|
26
|
+
<tbody>
|
27
|
+
</table
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module SidekiqUtils
|
2
|
+
module WebExtensions
|
3
|
+
module JobCounter
|
4
|
+
def self.registered(app)
|
5
|
+
view_path = File.join(File.expand_path("..", __FILE__), "views")
|
6
|
+
|
7
|
+
require 'active_support/number_helper'
|
8
|
+
app.get("/job_counts") do
|
9
|
+
@throughput = SidekiqUtils::RedisMonitorStorage.
|
10
|
+
retrieve('sidekiq_elapsed', 'elapsed')
|
11
|
+
@counts = []
|
12
|
+
SidekiqUtils::JobCounter.counts.each do |queue, job_counts|
|
13
|
+
job_counts.each do |job, count|
|
14
|
+
values = { queue: queue, job: job, count: count }
|
15
|
+
if (values[:avg_runtime] = @throughput[job].try!(:[], 'average'))
|
16
|
+
execution_time = values[:count] * values[:avg_runtime].to_f / 1_000
|
17
|
+
values[:runtime_day] = days =
|
18
|
+
(execution_time / 1.day).floor
|
19
|
+
values[:runtime_hour] = hours =
|
20
|
+
((execution_time - days.days) / 1.hour).floor
|
21
|
+
values[:runtime_min] = (
|
22
|
+
(execution_time - days.days - hours.hours) / 1.minute
|
23
|
+
).round
|
24
|
+
end
|
25
|
+
@counts << values
|
26
|
+
end
|
27
|
+
end
|
28
|
+
@counts.sort_by! {|x| -1 * x[:count] }
|
29
|
+
|
30
|
+
render(:erb, File.read(File.join(view_path, "job_counts.erb")))
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module SidekiqUtils
|
2
|
+
module WebExtensions
|
3
|
+
module MemoryMonitor
|
4
|
+
def self.registered(app)
|
5
|
+
view_path = File.join(File.expand_path("..", __FILE__), "views")
|
6
|
+
|
7
|
+
require 'active_support/number_helper'
|
8
|
+
app.get("/memory") do
|
9
|
+
memory = SidekiqUtils::RedisMonitorStorage.retrieve('sidekiq_memory', 'memory')
|
10
|
+
object = SidekiqUtils::RedisMonitorStorage.retrieve('sidekiq_memory', 'object')
|
11
|
+
|
12
|
+
@memory = (memory.keys | object.keys).map do |job|
|
13
|
+
[job,
|
14
|
+
memory[job]['average'],
|
15
|
+
object[job]['average'],
|
16
|
+
memory[job]['sum'],
|
17
|
+
object[job]['sum'],
|
18
|
+
]
|
19
|
+
end.sort_by {|x| -x[3] }
|
20
|
+
|
21
|
+
render(:erb, File.read(File.join(view_path, "memory.erb")))
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module SidekiqUtils
|
2
|
+
module WebExtensions
|
3
|
+
module ThroughputMonitor
|
4
|
+
def self.registered(app)
|
5
|
+
view_path = File.join(File.expand_path("..", __FILE__), "views")
|
6
|
+
|
7
|
+
app.get("/throughput") do
|
8
|
+
last_run_at = {}
|
9
|
+
Sidekiq.redis {|r| r.hgetall('sidekiq_last_run') }.each do |job, last_run|
|
10
|
+
last_run_at[job] = last_run
|
11
|
+
end
|
12
|
+
@throughput = SidekiqUtils::RedisMonitorStorage.
|
13
|
+
retrieve('sidekiq_elapsed', 'elapsed').map do |job, values|
|
14
|
+
|
15
|
+
if last_run_at[job]
|
16
|
+
last_run_time = Time.at(Integer(last_run_at[job])).
|
17
|
+
in_time_zone('US/Eastern').to_s(:long) + ' ET'
|
18
|
+
else
|
19
|
+
last_run_time = 'n/a'
|
20
|
+
end
|
21
|
+
[job,
|
22
|
+
values['average'],
|
23
|
+
values['count'],
|
24
|
+
last_run_time,
|
25
|
+
]
|
26
|
+
end.sort_by {|x| -x[2] }
|
27
|
+
render(:erb, File.read(File.join(view_path, "throughput.erb")))
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'sidekiq_utils/middleware/client/additional_serialization'
|
2
|
+
require 'sidekiq_utils/middleware/client/deprioritize'
|
3
|
+
require 'sidekiq_utils/middleware/client/job_counter'
|
4
|
+
|
5
|
+
require 'sidekiq_utils/middleware/server/additional_serialization'
|
6
|
+
require 'sidekiq_utils/middleware/server/find_optional'
|
7
|
+
require 'sidekiq_utils/middleware/server/job_counter'
|
8
|
+
require 'sidekiq_utils/middleware/server/memory_monitor'
|
9
|
+
require 'sidekiq_utils/middleware/server/throughput_monitor'
|
10
|
+
|
11
|
+
require 'sidekiq_utils/web_extensions/job_counter'
|
12
|
+
require 'sidekiq_utils/web_extensions/memory_monitor'
|
13
|
+
require 'sidekiq_utils/web_extensions/throughput_monitor'
|
14
|
+
|
15
|
+
require 'sidekiq_utils/redis_monitor_storage'
|
16
|
+
require 'sidekiq_utils/additional_serialization'
|
17
|
+
require 'sidekiq_utils/deprioritize'
|
18
|
+
require 'sidekiq_utils/enqueued_jobs_helper'
|
19
|
+
require 'sidekiq_utils/find_optional'
|
20
|
+
require 'sidekiq_utils/job_counter'
|
21
|
+
require 'sidekiq_utils/latency_alert'
|
@@ -0,0 +1,84 @@
|
|
1
|
+
# Generated by juwelier
|
2
|
+
# DO NOT EDIT THIS FILE DIRECTLY
|
3
|
+
# Instead, edit Juwelier::Tasks in Rakefile, and run 'rake gemspec'
|
4
|
+
# -*- encoding: utf-8 -*-
|
5
|
+
# stub: sidekiq_utils 1.0.0 ruby lib
|
6
|
+
|
7
|
+
Gem::Specification.new do |s|
|
8
|
+
s.name = "sidekiq_utils".freeze
|
9
|
+
s.version = "1.0.0"
|
10
|
+
|
11
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0".freeze) if s.respond_to? :required_rubygems_version=
|
12
|
+
s.require_paths = ["lib".freeze]
|
13
|
+
s.authors = ["Magnus von Koeller".freeze]
|
14
|
+
s.date = "2017-12-08"
|
15
|
+
s.description = "Tools that make working with a major Sidekiq installation more fun.".freeze
|
16
|
+
s.email = "magnus@angel.co".freeze
|
17
|
+
s.extra_rdoc_files = [
|
18
|
+
"LICENSE",
|
19
|
+
"README.md"
|
20
|
+
]
|
21
|
+
s.files = [
|
22
|
+
"Gemfile",
|
23
|
+
"Gemfile.lock",
|
24
|
+
"LICENSE",
|
25
|
+
"README.md",
|
26
|
+
"Rakefile",
|
27
|
+
"VERSION",
|
28
|
+
"lib/sidekiq_utils.rb",
|
29
|
+
"lib/sidekiq_utils/additional_serialization.rb",
|
30
|
+
"lib/sidekiq_utils/deprioritize.rb",
|
31
|
+
"lib/sidekiq_utils/enqueued_jobs_helper.rb",
|
32
|
+
"lib/sidekiq_utils/find_optional.rb",
|
33
|
+
"lib/sidekiq_utils/job_counter.rb",
|
34
|
+
"lib/sidekiq_utils/latency_alert.rb",
|
35
|
+
"lib/sidekiq_utils/middleware/client/additional_serialization.rb",
|
36
|
+
"lib/sidekiq_utils/middleware/client/deprioritize.rb",
|
37
|
+
"lib/sidekiq_utils/middleware/client/job_counter.rb",
|
38
|
+
"lib/sidekiq_utils/middleware/server/additional_serialization.rb",
|
39
|
+
"lib/sidekiq_utils/middleware/server/find_optional.rb",
|
40
|
+
"lib/sidekiq_utils/middleware/server/job_counter.rb",
|
41
|
+
"lib/sidekiq_utils/middleware/server/memory_monitor.rb",
|
42
|
+
"lib/sidekiq_utils/middleware/server/throughput_monitor.rb",
|
43
|
+
"lib/sidekiq_utils/redis_monitor_storage.rb",
|
44
|
+
"lib/sidekiq_utils/views/job_counts.erb",
|
45
|
+
"lib/sidekiq_utils/views/memory.erb",
|
46
|
+
"lib/sidekiq_utils/views/throughput.erb",
|
47
|
+
"lib/sidekiq_utils/web_extensions/job_counter.rb",
|
48
|
+
"lib/sidekiq_utils/web_extensions/memory_monitor.rb",
|
49
|
+
"lib/sidekiq_utils/web_extensions/throughput_monitor.rb",
|
50
|
+
"sidekiq_utils.gemspec"
|
51
|
+
]
|
52
|
+
s.homepage = "http://github.com/venturehacks/sidekiq_angels".freeze
|
53
|
+
s.licenses = ["MIT".freeze]
|
54
|
+
s.rubygems_version = "2.6.13".freeze
|
55
|
+
s.summary = "Tools that make working with a major Sidekiq installation more fun.".freeze
|
56
|
+
|
57
|
+
if s.respond_to? :specification_version then
|
58
|
+
s.specification_version = 4
|
59
|
+
|
60
|
+
if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
|
61
|
+
s.add_runtime_dependency(%q<sidekiq>.freeze, [">= 4.0.0"])
|
62
|
+
s.add_development_dependency(%q<shoulda>.freeze, [">= 0"])
|
63
|
+
s.add_development_dependency(%q<rdoc>.freeze, ["~> 3.12"])
|
64
|
+
s.add_development_dependency(%q<bundler>.freeze, ["~> 1.0"])
|
65
|
+
s.add_development_dependency(%q<juwelier>.freeze, ["~> 2.1.0"])
|
66
|
+
s.add_development_dependency(%q<simplecov>.freeze, [">= 0"])
|
67
|
+
else
|
68
|
+
s.add_dependency(%q<sidekiq>.freeze, [">= 4.0.0"])
|
69
|
+
s.add_dependency(%q<shoulda>.freeze, [">= 0"])
|
70
|
+
s.add_dependency(%q<rdoc>.freeze, ["~> 3.12"])
|
71
|
+
s.add_dependency(%q<bundler>.freeze, ["~> 1.0"])
|
72
|
+
s.add_dependency(%q<juwelier>.freeze, ["~> 2.1.0"])
|
73
|
+
s.add_dependency(%q<simplecov>.freeze, [">= 0"])
|
74
|
+
end
|
75
|
+
else
|
76
|
+
s.add_dependency(%q<sidekiq>.freeze, [">= 4.0.0"])
|
77
|
+
s.add_dependency(%q<shoulda>.freeze, [">= 0"])
|
78
|
+
s.add_dependency(%q<rdoc>.freeze, ["~> 3.12"])
|
79
|
+
s.add_dependency(%q<bundler>.freeze, ["~> 1.0"])
|
80
|
+
s.add_dependency(%q<juwelier>.freeze, ["~> 2.1.0"])
|
81
|
+
s.add_dependency(%q<simplecov>.freeze, [">= 0"])
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|