sidekiq 5.2.7 → 6.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.
- checksums.yaml +4 -4
- data/.gitignore +0 -2
- data/.standard.yml +20 -0
- data/6.0-Upgrade.md +70 -0
- data/Changes.md +34 -0
- data/Ent-2.0-Upgrade.md +37 -0
- data/Ent-Changes.md +12 -0
- data/Gemfile +12 -11
- data/Gemfile.lock +196 -0
- data/Pro-5.0-Upgrade.md +25 -0
- data/Pro-Changes.md +12 -3
- data/README.md +16 -30
- data/Rakefile +5 -4
- data/bin/sidekiqload +26 -22
- data/bin/sidekiqmon +9 -0
- data/lib/generators/sidekiq/templates/worker_test.rb.erb +1 -1
- data/lib/generators/sidekiq/worker_generator.rb +12 -14
- data/lib/sidekiq.rb +53 -42
- data/lib/sidekiq/api.rb +138 -151
- data/lib/sidekiq/cli.rb +97 -162
- data/lib/sidekiq/client.rb +45 -46
- data/lib/sidekiq/delay.rb +5 -6
- data/lib/sidekiq/exception_handler.rb +10 -12
- data/lib/sidekiq/extensions/action_mailer.rb +10 -20
- data/lib/sidekiq/extensions/active_record.rb +9 -7
- data/lib/sidekiq/extensions/class_methods.rb +9 -7
- data/lib/sidekiq/extensions/generic_proxy.rb +4 -4
- data/lib/sidekiq/fetch.rb +5 -6
- data/lib/sidekiq/job_logger.rb +37 -7
- data/lib/sidekiq/job_retry.rb +45 -58
- data/lib/sidekiq/launcher.rb +59 -51
- data/lib/sidekiq/logger.rb +69 -0
- data/lib/sidekiq/manager.rb +7 -9
- data/lib/sidekiq/middleware/chain.rb +3 -2
- data/lib/sidekiq/middleware/i18n.rb +5 -7
- data/lib/sidekiq/monitor.rb +148 -0
- data/lib/sidekiq/paginator.rb +11 -12
- data/lib/sidekiq/processor.rb +52 -49
- data/lib/sidekiq/rails.rb +23 -29
- data/lib/sidekiq/redis_connection.rb +31 -37
- data/lib/sidekiq/scheduled.rb +17 -19
- data/lib/sidekiq/testing.rb +22 -23
- data/lib/sidekiq/testing/inline.rb +2 -1
- data/lib/sidekiq/util.rb +17 -14
- data/lib/sidekiq/version.rb +2 -1
- data/lib/sidekiq/web.rb +41 -49
- data/lib/sidekiq/web/action.rb +14 -10
- data/lib/sidekiq/web/application.rb +60 -57
- data/lib/sidekiq/web/helpers.rb +66 -67
- data/lib/sidekiq/web/router.rb +17 -14
- data/lib/sidekiq/worker.rb +124 -97
- data/sidekiq.gemspec +16 -16
- data/web/assets/javascripts/dashboard.js +2 -21
- data/web/locales/ja.yml +2 -1
- metadata +19 -29
- data/.travis.yml +0 -11
- data/bin/sidekiqctl +0 -20
- data/lib/sidekiq/core_ext.rb +0 -1
- data/lib/sidekiq/ctl.rb +0 -221
- data/lib/sidekiq/logging.rb +0 -122
- data/lib/sidekiq/middleware/server/active_record.rb +0 -23
data/lib/sidekiq/launcher.rb
CHANGED
@@ -1,19 +1,25 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require
|
4
|
-
require
|
2
|
+
|
3
|
+
require "sidekiq/manager"
|
4
|
+
require "sidekiq/fetch"
|
5
|
+
require "sidekiq/scheduled"
|
5
6
|
|
6
7
|
module Sidekiq
|
7
|
-
# The Launcher
|
8
|
-
# start, monitor and stop the core Actors in Sidekiq.
|
9
|
-
# If any of these actors die, the Sidekiq process exits
|
10
|
-
# immediately.
|
8
|
+
# The Launcher starts the Manager and Poller threads and provides the process heartbeat.
|
11
9
|
class Launcher
|
12
10
|
include Util
|
13
11
|
|
14
|
-
|
12
|
+
STATS_TTL = 5 * 365 * 24 * 60 * 60 # 5 years
|
15
13
|
|
16
|
-
|
14
|
+
PROCTITLES = [
|
15
|
+
proc { "sidekiq" },
|
16
|
+
proc { Sidekiq::VERSION },
|
17
|
+
proc { |me, data| data["tag"] },
|
18
|
+
proc { |me, data| "[#{Processor::WORKER_STATE.size} of #{data["concurrency"]} busy]" },
|
19
|
+
proc { |me, data| "stopping" if me.stopping? },
|
20
|
+
]
|
21
|
+
|
22
|
+
attr_accessor :manager, :poller, :fetcher
|
17
23
|
|
18
24
|
def initialize(options)
|
19
25
|
@manager = Sidekiq::Manager.new(options)
|
@@ -62,10 +68,30 @@ module Sidekiq
|
|
62
68
|
|
63
69
|
private unless $TESTING
|
64
70
|
|
71
|
+
def start_heartbeat
|
72
|
+
loop do
|
73
|
+
heartbeat
|
74
|
+
sleep 5
|
75
|
+
end
|
76
|
+
Sidekiq.logger.info("Heartbeat stopping...")
|
77
|
+
end
|
78
|
+
|
79
|
+
def clear_heartbeat
|
80
|
+
# Remove record from Redis since we are shutting down.
|
81
|
+
# Note we don't stop the heartbeat thread; if the process
|
82
|
+
# doesn't actually exit, it'll reappear in the Web UI.
|
83
|
+
Sidekiq.redis do |conn|
|
84
|
+
conn.pipelined do
|
85
|
+
conn.srem("processes", identity)
|
86
|
+
conn.del("#{identity}:workers")
|
87
|
+
end
|
88
|
+
end
|
89
|
+
rescue
|
90
|
+
# best effort, ignore network errors
|
91
|
+
end
|
92
|
+
|
65
93
|
def heartbeat
|
66
|
-
|
67
|
-
results.compact!
|
68
|
-
$0 = results.join(' ')
|
94
|
+
$0 = PROCTITLES.map { |proc| proc.call(self, to_data) }.compact.join(" ")
|
69
95
|
|
70
96
|
❤
|
71
97
|
end
|
@@ -73,6 +99,7 @@ module Sidekiq
|
|
73
99
|
def ❤
|
74
100
|
key = identity
|
75
101
|
fails = procd = 0
|
102
|
+
|
76
103
|
begin
|
77
104
|
fails = Processor::FAILURE.reset
|
78
105
|
procd = Processor::PROCESSED.reset
|
@@ -80,6 +107,7 @@ module Sidekiq
|
|
80
107
|
|
81
108
|
workers_key = "#{key}:workers"
|
82
109
|
nowdate = Time.now.utc.strftime("%Y-%m-%d")
|
110
|
+
|
83
111
|
Sidekiq.redis do |conn|
|
84
112
|
conn.multi do
|
85
113
|
conn.incrby("stat:processed", procd)
|
@@ -97,24 +125,27 @@ module Sidekiq
|
|
97
125
|
conn.expire(workers_key, 60)
|
98
126
|
end
|
99
127
|
end
|
128
|
+
|
100
129
|
fails = procd = 0
|
101
130
|
|
102
|
-
_, exists, _, _, msg = Sidekiq.redis
|
103
|
-
conn.multi
|
104
|
-
conn.sadd(
|
131
|
+
_, exists, _, _, msg = Sidekiq.redis { |conn|
|
132
|
+
res = conn.multi {
|
133
|
+
conn.sadd("processes", key)
|
105
134
|
conn.exists(key)
|
106
|
-
conn.hmset(key,
|
135
|
+
conn.hmset(key, "info", to_json, "busy", curstate.size, "beat", Time.now.to_f, "quiet", @done)
|
107
136
|
conn.expire(key, 60)
|
108
137
|
conn.rpop("#{key}-signals")
|
109
|
-
|
110
|
-
|
138
|
+
}
|
139
|
+
|
140
|
+
res
|
141
|
+
}
|
111
142
|
|
112
143
|
# first heartbeat or recovering from an outage and need to reestablish our heartbeat
|
113
|
-
fire_event(:heartbeat)
|
144
|
+
fire_event(:heartbeat) unless exists
|
114
145
|
|
115
146
|
return unless msg
|
116
147
|
|
117
|
-
::Process.kill(msg,
|
148
|
+
::Process.kill(msg, ::Process.pid)
|
118
149
|
rescue => e
|
119
150
|
# ignore all redis/network issues
|
120
151
|
logger.error("heartbeat: #{e.message}")
|
@@ -124,25 +155,17 @@ module Sidekiq
|
|
124
155
|
end
|
125
156
|
end
|
126
157
|
|
127
|
-
def start_heartbeat
|
128
|
-
while true
|
129
|
-
heartbeat
|
130
|
-
sleep 5
|
131
|
-
end
|
132
|
-
Sidekiq.logger.info("Heartbeat stopping...")
|
133
|
-
end
|
134
|
-
|
135
158
|
def to_data
|
136
159
|
@data ||= begin
|
137
160
|
{
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
161
|
+
"hostname" => hostname,
|
162
|
+
"started_at" => Time.now.to_f,
|
163
|
+
"pid" => ::Process.pid,
|
164
|
+
"tag" => @options[:tag] || "",
|
165
|
+
"concurrency" => @options[:concurrency],
|
166
|
+
"queues" => @options[:queues].uniq,
|
167
|
+
"labels" => @options[:labels],
|
168
|
+
"identity" => identity,
|
146
169
|
}
|
147
170
|
end
|
148
171
|
end
|
@@ -154,20 +177,5 @@ module Sidekiq
|
|
154
177
|
Sidekiq.dump_json(to_data)
|
155
178
|
end
|
156
179
|
end
|
157
|
-
|
158
|
-
def clear_heartbeat
|
159
|
-
# Remove record from Redis since we are shutting down.
|
160
|
-
# Note we don't stop the heartbeat thread; if the process
|
161
|
-
# doesn't actually exit, it'll reappear in the Web UI.
|
162
|
-
Sidekiq.redis do |conn|
|
163
|
-
conn.pipelined do
|
164
|
-
conn.srem('processes', identity)
|
165
|
-
conn.del("#{identity}:workers")
|
166
|
-
end
|
167
|
-
end
|
168
|
-
rescue
|
169
|
-
# best effort, ignore network errors
|
170
|
-
end
|
171
|
-
|
172
180
|
end
|
173
181
|
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "logger"
|
4
|
+
require "time"
|
5
|
+
|
6
|
+
module Sidekiq
|
7
|
+
class Logger < ::Logger
|
8
|
+
def initialize(*args)
|
9
|
+
super
|
10
|
+
|
11
|
+
self.formatter = Sidekiq.log_formatter
|
12
|
+
end
|
13
|
+
|
14
|
+
def with_context(hash)
|
15
|
+
ctx.merge!(hash)
|
16
|
+
yield
|
17
|
+
ensure
|
18
|
+
hash.keys.each { |key| ctx.delete(key) }
|
19
|
+
end
|
20
|
+
|
21
|
+
def ctx
|
22
|
+
Thread.current[:sidekiq_context] ||= {}
|
23
|
+
end
|
24
|
+
|
25
|
+
module Formatters
|
26
|
+
class Base < ::Logger::Formatter
|
27
|
+
def tid
|
28
|
+
Thread.current["sidekiq_tid"] ||= (Thread.current.object_id ^ ::Process.pid).to_s(36)
|
29
|
+
end
|
30
|
+
|
31
|
+
def ctx
|
32
|
+
Thread.current[:sidekiq_context] ||= {}
|
33
|
+
end
|
34
|
+
|
35
|
+
def format_context
|
36
|
+
" " + ctx.compact.map { |k, v| "#{k}=#{v}" }.join(" ") if ctx.any?
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
class Pretty < Base
|
41
|
+
def call(severity, time, program_name, message)
|
42
|
+
"#{time.utc.iso8601(3)} pid=#{::Process.pid} tid=#{tid}#{format_context} #{severity}: #{message}\n"
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
class WithoutTimestamp < Pretty
|
47
|
+
def call(severity, time, program_name, message)
|
48
|
+
"pid=#{::Process.pid} tid=#{tid}#{format_context} #{severity}: #{message}\n"
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
class JSON < Base
|
53
|
+
def call(severity, time, program_name, message)
|
54
|
+
hash = {
|
55
|
+
ts: time.utc.iso8601(3),
|
56
|
+
pid: ::Process.pid,
|
57
|
+
tid: tid,
|
58
|
+
lvl: severity,
|
59
|
+
msg: message,
|
60
|
+
}
|
61
|
+
c = ctx
|
62
|
+
hash["ctx"] = c unless c.empty?
|
63
|
+
|
64
|
+
Sidekiq.dump_json(hash) << "\n"
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
data/lib/sidekiq/manager.rb
CHANGED
@@ -1,12 +1,11 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
-
require 'sidekiq/util'
|
3
|
-
require 'sidekiq/processor'
|
4
|
-
require 'sidekiq/fetch'
|
5
|
-
require 'thread'
|
6
|
-
require 'set'
|
7
2
|
|
8
|
-
|
3
|
+
require "sidekiq/util"
|
4
|
+
require "sidekiq/processor"
|
5
|
+
require "sidekiq/fetch"
|
6
|
+
require "set"
|
9
7
|
|
8
|
+
module Sidekiq
|
10
9
|
##
|
11
10
|
# The Manager is the central coordination point in Sidekiq, controlling
|
12
11
|
# the lifecycle of the Processors.
|
@@ -27,7 +26,7 @@ module Sidekiq
|
|
27
26
|
attr_reader :workers
|
28
27
|
attr_reader :options
|
29
28
|
|
30
|
-
def initialize(options={})
|
29
|
+
def initialize(options = {})
|
31
30
|
logger.debug { options.inspect }
|
32
31
|
@options = options
|
33
32
|
@count = options[:concurrency] || 10
|
@@ -113,7 +112,7 @@ module Sidekiq
|
|
113
112
|
end
|
114
113
|
|
115
114
|
if cleanup.size > 0
|
116
|
-
jobs = cleanup.map {|p| p.job }.compact
|
115
|
+
jobs = cleanup.map { |p| p.job }.compact
|
117
116
|
|
118
117
|
logger.warn { "Terminating #{cleanup.size} busy worker threads" }
|
119
118
|
logger.warn { "Work still in progress #{jobs.inspect}" }
|
@@ -132,6 +131,5 @@ module Sidekiq
|
|
132
131
|
processor.kill
|
133
132
|
end
|
134
133
|
end
|
135
|
-
|
136
134
|
end
|
137
135
|
end
|
@@ -1,4 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
|
2
3
|
module Sidekiq
|
3
4
|
# Middleware is code configured to run before/after
|
4
5
|
# a message is processed. It is patterned after Rack
|
@@ -106,7 +107,7 @@ module Sidekiq
|
|
106
107
|
i = entries.index { |entry| entry.klass == newklass }
|
107
108
|
new_entry = i.nil? ? Entry.new(newklass, *args) : entries.delete_at(i)
|
108
109
|
i = entries.index { |entry| entry.klass == oldklass } || entries.count - 1
|
109
|
-
entries.insert(i+1, new_entry)
|
110
|
+
entries.insert(i + 1, new_entry)
|
110
111
|
end
|
111
112
|
|
112
113
|
def exists?(klass)
|
@@ -139,7 +140,7 @@ module Sidekiq
|
|
139
140
|
|
140
141
|
def initialize(klass, *args)
|
141
142
|
@klass = klass
|
142
|
-
@args
|
143
|
+
@args = args
|
143
144
|
end
|
144
145
|
|
145
146
|
def make_new
|
@@ -1,4 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
|
2
3
|
#
|
3
4
|
# Simple middleware to save the current locale and restore it when the job executes.
|
4
5
|
# Use it by requiring it in your initializer:
|
@@ -9,19 +10,16 @@ module Sidekiq::Middleware::I18n
|
|
9
10
|
# Get the current locale and store it in the message
|
10
11
|
# to be sent to Sidekiq.
|
11
12
|
class Client
|
12
|
-
def call(
|
13
|
-
msg[
|
13
|
+
def call(_worker, msg, _queue, _redis)
|
14
|
+
msg["locale"] ||= I18n.locale
|
14
15
|
yield
|
15
16
|
end
|
16
17
|
end
|
17
18
|
|
18
19
|
# Pull the msg locale out and set the current thread to use it.
|
19
20
|
class Server
|
20
|
-
def call(
|
21
|
-
I18n.
|
22
|
-
yield
|
23
|
-
ensure
|
24
|
-
I18n.locale = I18n.default_locale
|
21
|
+
def call(_worker, msg, _queue, &block)
|
22
|
+
I18n.with_locale(msg.fetch("locale", I18n.default_locale), &block)
|
25
23
|
end
|
26
24
|
end
|
27
25
|
end
|
@@ -0,0 +1,148 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "fileutils"
|
4
|
+
require "sidekiq/api"
|
5
|
+
|
6
|
+
class Sidekiq::Monitor
|
7
|
+
CMD = File.basename($PROGRAM_NAME)
|
8
|
+
|
9
|
+
attr_reader :stage
|
10
|
+
|
11
|
+
def self.print_usage
|
12
|
+
puts "#{CMD} - monitor Sidekiq from the command line."
|
13
|
+
puts
|
14
|
+
puts "Usage: #{CMD} status <section>"
|
15
|
+
puts
|
16
|
+
puts " <section> (optional) view a specific section of the status output"
|
17
|
+
puts " Valid sections are: #{Sidekiq::Monitor::Status::VALID_SECTIONS.join(", ")}"
|
18
|
+
puts
|
19
|
+
puts "Set REDIS_URL to the location of your Redis server if not monitoring localhost."
|
20
|
+
end
|
21
|
+
|
22
|
+
class Status
|
23
|
+
VALID_SECTIONS = %w[all version overview processes queues]
|
24
|
+
COL_PAD = 2
|
25
|
+
|
26
|
+
def display(section = nil)
|
27
|
+
section ||= "all"
|
28
|
+
unless VALID_SECTIONS.include? section
|
29
|
+
puts "I don't know how to check the status of '#{section}'!"
|
30
|
+
puts "Try one of these: #{VALID_SECTIONS.join(", ")}"
|
31
|
+
return
|
32
|
+
end
|
33
|
+
send(section)
|
34
|
+
rescue => e
|
35
|
+
puts "Couldn't get status: #{e}"
|
36
|
+
end
|
37
|
+
|
38
|
+
def all
|
39
|
+
version
|
40
|
+
puts
|
41
|
+
overview
|
42
|
+
puts
|
43
|
+
processes
|
44
|
+
puts
|
45
|
+
queues
|
46
|
+
end
|
47
|
+
|
48
|
+
def version
|
49
|
+
puts "Sidekiq #{Sidekiq::VERSION}"
|
50
|
+
puts Time.now
|
51
|
+
end
|
52
|
+
|
53
|
+
def overview
|
54
|
+
puts "---- Overview ----"
|
55
|
+
puts " Processed: #{delimit stats.processed}"
|
56
|
+
puts " Failed: #{delimit stats.failed}"
|
57
|
+
puts " Busy: #{delimit stats.workers_size}"
|
58
|
+
puts " Enqueued: #{delimit stats.enqueued}"
|
59
|
+
puts " Retries: #{delimit stats.retry_size}"
|
60
|
+
puts " Scheduled: #{delimit stats.scheduled_size}"
|
61
|
+
puts " Dead: #{delimit stats.dead_size}"
|
62
|
+
end
|
63
|
+
|
64
|
+
def processes
|
65
|
+
puts "---- Processes (#{process_set.size}) ----"
|
66
|
+
process_set.each_with_index do |process, index|
|
67
|
+
puts "#{process["identity"]} #{tags_for(process)}"
|
68
|
+
puts " Started: #{Time.at(process["started_at"])} (#{time_ago(process["started_at"])})"
|
69
|
+
puts " Threads: #{process["concurrency"]} (#{process["busy"]} busy)"
|
70
|
+
puts " Queues: #{split_multiline(process["queues"].sort, pad: 11)}"
|
71
|
+
puts "" unless (index + 1) == process_set.size
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def queues
|
76
|
+
puts "---- Queues (#{queue_data.size}) ----"
|
77
|
+
columns = {
|
78
|
+
name: [:ljust, (["name"] + queue_data.map(&:name)).map(&:length).max + COL_PAD],
|
79
|
+
size: [:rjust, (["size"] + queue_data.map(&:size)).map(&:length).max + COL_PAD],
|
80
|
+
latency: [:rjust, (["latency"] + queue_data.map(&:latency)).map(&:length).max + COL_PAD],
|
81
|
+
}
|
82
|
+
columns.each { |col, (dir, width)| print col.to_s.upcase.public_send(dir, width) }
|
83
|
+
puts
|
84
|
+
queue_data.each do |q|
|
85
|
+
columns.each do |col, (dir, width)|
|
86
|
+
print q.send(col).public_send(dir, width)
|
87
|
+
end
|
88
|
+
puts
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
private
|
93
|
+
|
94
|
+
def delimit(number)
|
95
|
+
number.to_s.reverse.scan(/.{1,3}/).join(",").reverse
|
96
|
+
end
|
97
|
+
|
98
|
+
def split_multiline(values, opts = {})
|
99
|
+
return "none" unless values
|
100
|
+
pad = opts[:pad] || 0
|
101
|
+
max_length = opts[:max_length] || (80 - pad)
|
102
|
+
out = []
|
103
|
+
line = ""
|
104
|
+
values.each do |value|
|
105
|
+
if (line.length + value.length) > max_length
|
106
|
+
out << line
|
107
|
+
line = " " * pad
|
108
|
+
end
|
109
|
+
line << value + ", "
|
110
|
+
end
|
111
|
+
out << line[0..-3]
|
112
|
+
out.join("\n")
|
113
|
+
end
|
114
|
+
|
115
|
+
def tags_for(process)
|
116
|
+
tags = [
|
117
|
+
process["tag"],
|
118
|
+
process["labels"],
|
119
|
+
(process["quiet"] == "true" ? "quiet" : nil),
|
120
|
+
].flatten.compact
|
121
|
+
tags.any? ? "[#{tags.join("] [")}]" : nil
|
122
|
+
end
|
123
|
+
|
124
|
+
def time_ago(timestamp)
|
125
|
+
seconds = Time.now - Time.at(timestamp)
|
126
|
+
return "just now" if seconds < 60
|
127
|
+
return "a minute ago" if seconds < 120
|
128
|
+
return "#{seconds.floor / 60} minutes ago" if seconds < 3600
|
129
|
+
return "an hour ago" if seconds < 7200
|
130
|
+
"#{seconds.floor / 60 / 60} hours ago"
|
131
|
+
end
|
132
|
+
|
133
|
+
QUEUE_STRUCT = Struct.new(:name, :size, :latency)
|
134
|
+
def queue_data
|
135
|
+
@queue_data ||= Sidekiq::Queue.all.map { |q|
|
136
|
+
QUEUE_STRUCT.new(q.name, q.size.to_s, sprintf("%#.2f", q.latency))
|
137
|
+
}
|
138
|
+
end
|
139
|
+
|
140
|
+
def process_set
|
141
|
+
@process_set ||= Sidekiq::ProcessSet.new
|
142
|
+
end
|
143
|
+
|
144
|
+
def stats
|
145
|
+
@stats ||= Sidekiq::Stats.new
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|