mini-clean-lib 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/mini-clean-lib.gemspec +12 -0
- data/sidekiq-8.1.6/Changes.md +2515 -0
- data/sidekiq-8.1.6/LICENSE.txt +9 -0
- data/sidekiq-8.1.6/README.md +124 -0
- data/sidekiq-8.1.6/bin/kiq +17 -0
- data/sidekiq-8.1.6/bin/lint-herb +13 -0
- data/sidekiq-8.1.6/bin/multi_queue_bench +271 -0
- data/sidekiq-8.1.6/bin/sidekiq +37 -0
- data/sidekiq-8.1.6/bin/sidekiqload +256 -0
- data/sidekiq-8.1.6/bin/sidekiqmon +11 -0
- data/sidekiq-8.1.6/bin/webload +69 -0
- data/sidekiq-8.1.6/lib/active_job/queue_adapters/sidekiq_adapter.rb +124 -0
- data/sidekiq-8.1.6/lib/generators/sidekiq/job_generator.rb +71 -0
- data/sidekiq-8.1.6/lib/generators/sidekiq/templates/job.rb.erb +9 -0
- data/sidekiq-8.1.6/lib/generators/sidekiq/templates/job_spec.rb.erb +6 -0
- data/sidekiq-8.1.6/lib/generators/sidekiq/templates/job_test.rb.erb +8 -0
- data/sidekiq-8.1.6/lib/sidekiq/api.rb +1466 -0
- data/sidekiq-8.1.6/lib/sidekiq/capsule.rb +135 -0
- data/sidekiq-8.1.6/lib/sidekiq/cli.rb +433 -0
- data/sidekiq-8.1.6/lib/sidekiq/client.rb +306 -0
- data/sidekiq-8.1.6/lib/sidekiq/component.rb +132 -0
- data/sidekiq-8.1.6/lib/sidekiq/config.rb +316 -0
- data/sidekiq-8.1.6/lib/sidekiq/deploy.rb +64 -0
- data/sidekiq-8.1.6/lib/sidekiq/embedded.rb +64 -0
- data/sidekiq-8.1.6/lib/sidekiq/fetch.rb +89 -0
- data/sidekiq-8.1.6/lib/sidekiq/iterable_job.rb +56 -0
- data/sidekiq-8.1.6/lib/sidekiq/job/interrupt_handler.rb +24 -0
- data/sidekiq-8.1.6/lib/sidekiq/job/iterable/active_record_enumerator.rb +53 -0
- data/sidekiq-8.1.6/lib/sidekiq/job/iterable/csv_enumerator.rb +47 -0
- data/sidekiq-8.1.6/lib/sidekiq/job/iterable/enumerators.rb +135 -0
- data/sidekiq-8.1.6/lib/sidekiq/job/iterable.rb +322 -0
- data/sidekiq-8.1.6/lib/sidekiq/job.rb +400 -0
- data/sidekiq-8.1.6/lib/sidekiq/job_logger.rb +54 -0
- data/sidekiq-8.1.6/lib/sidekiq/job_retry.rb +332 -0
- data/sidekiq-8.1.6/lib/sidekiq/job_util.rb +113 -0
- data/sidekiq-8.1.6/lib/sidekiq/launcher.rb +283 -0
- data/sidekiq-8.1.6/lib/sidekiq/loader.rb +57 -0
- data/sidekiq-8.1.6/lib/sidekiq/logger.rb +87 -0
- data/sidekiq-8.1.6/lib/sidekiq/manager.rb +134 -0
- data/sidekiq-8.1.6/lib/sidekiq/metrics/query.rb +184 -0
- data/sidekiq-8.1.6/lib/sidekiq/metrics/shared.rb +109 -0
- data/sidekiq-8.1.6/lib/sidekiq/metrics/tracking.rb +153 -0
- data/sidekiq-8.1.6/lib/sidekiq/middleware/chain.rb +207 -0
- data/sidekiq-8.1.6/lib/sidekiq/middleware/current_attributes.rb +120 -0
- data/sidekiq-8.1.6/lib/sidekiq/middleware/i18n.rb +44 -0
- data/sidekiq-8.1.6/lib/sidekiq/middleware/modules.rb +23 -0
- data/sidekiq-8.1.6/lib/sidekiq/monitor.rb +143 -0
- data/sidekiq-8.1.6/lib/sidekiq/paginator.rb +74 -0
- data/sidekiq-8.1.6/lib/sidekiq/processor.rb +298 -0
- data/sidekiq-8.1.6/lib/sidekiq/profiler.rb +73 -0
- data/sidekiq-8.1.6/lib/sidekiq/rails.rb +63 -0
- data/sidekiq-8.1.6/lib/sidekiq/redis_client_adapter.rb +117 -0
- data/sidekiq-8.1.6/lib/sidekiq/redis_connection.rb +115 -0
- data/sidekiq-8.1.6/lib/sidekiq/ring_buffer.rb +32 -0
- data/sidekiq-8.1.6/lib/sidekiq/scheduled.rb +235 -0
- data/sidekiq-8.1.6/lib/sidekiq/sd_notify.rb +149 -0
- data/sidekiq-8.1.6/lib/sidekiq/systemd.rb +26 -0
- data/sidekiq-8.1.6/lib/sidekiq/test_api.rb +331 -0
- data/sidekiq-8.1.6/lib/sidekiq/testing/inline.rb +2 -0
- data/sidekiq-8.1.6/lib/sidekiq/testing.rb +3 -0
- data/sidekiq-8.1.6/lib/sidekiq/transaction_aware_client.rb +59 -0
- data/sidekiq-8.1.6/lib/sidekiq/tui/controls.rb +53 -0
- data/sidekiq-8.1.6/lib/sidekiq/tui/filtering.rb +53 -0
- data/sidekiq-8.1.6/lib/sidekiq/tui/tabs/base_tab.rb +204 -0
- data/sidekiq-8.1.6/lib/sidekiq/tui/tabs/busy.rb +118 -0
- data/sidekiq-8.1.6/lib/sidekiq/tui/tabs/dead.rb +19 -0
- data/sidekiq-8.1.6/lib/sidekiq/tui/tabs/home.rb +144 -0
- data/sidekiq-8.1.6/lib/sidekiq/tui/tabs/metrics.rb +131 -0
- data/sidekiq-8.1.6/lib/sidekiq/tui/tabs/queues.rb +95 -0
- data/sidekiq-8.1.6/lib/sidekiq/tui/tabs/retries.rb +19 -0
- data/sidekiq-8.1.6/lib/sidekiq/tui/tabs/scheduled.rb +19 -0
- data/sidekiq-8.1.6/lib/sidekiq/tui/tabs/set_tab.rb +96 -0
- data/sidekiq-8.1.6/lib/sidekiq/tui/tabs.rb +15 -0
- data/sidekiq-8.1.6/lib/sidekiq/tui.rb +382 -0
- data/sidekiq-8.1.6/lib/sidekiq/version.rb +10 -0
- data/sidekiq-8.1.6/lib/sidekiq/web/action.rb +178 -0
- data/sidekiq-8.1.6/lib/sidekiq/web/application.rb +480 -0
- data/sidekiq-8.1.6/lib/sidekiq/web/config.rb +117 -0
- data/sidekiq-8.1.6/lib/sidekiq/web/helpers.rb +474 -0
- data/sidekiq-8.1.6/lib/sidekiq/web/router.rb +91 -0
- data/sidekiq-8.1.6/lib/sidekiq/web.rb +140 -0
- data/sidekiq-8.1.6/lib/sidekiq/worker_compatibility_alias.rb +13 -0
- data/sidekiq-8.1.6/lib/sidekiq.rb +161 -0
- data/sidekiq-8.1.6/sidekiq.gemspec +31 -0
- data/sidekiq-8.1.6/web/assets/images/apple-touch-icon.png +0 -0
- data/sidekiq-8.1.6/web/assets/images/favicon.ico +0 -0
- data/sidekiq-8.1.6/web/assets/images/logo.png +0 -0
- data/sidekiq-8.1.6/web/assets/images/status.png +0 -0
- data/sidekiq-8.1.6/web/assets/javascripts/application.js +204 -0
- data/sidekiq-8.1.6/web/assets/javascripts/base-charts.js +120 -0
- data/sidekiq-8.1.6/web/assets/javascripts/chart.min.js +13 -0
- data/sidekiq-8.1.6/web/assets/javascripts/chartjs-adapter-date-fns.min.js +7 -0
- data/sidekiq-8.1.6/web/assets/javascripts/chartjs-plugin-annotation.min.js +7 -0
- data/sidekiq-8.1.6/web/assets/javascripts/dashboard-charts.js +194 -0
- data/sidekiq-8.1.6/web/assets/javascripts/dashboard.js +63 -0
- data/sidekiq-8.1.6/web/assets/javascripts/metrics.js +280 -0
- data/sidekiq-8.1.6/web/assets/stylesheets/style.css +776 -0
- data/sidekiq-8.1.6/web/locales/ar.yml +89 -0
- data/sidekiq-8.1.6/web/locales/cs.yml +80 -0
- data/sidekiq-8.1.6/web/locales/da.yml +77 -0
- data/sidekiq-8.1.6/web/locales/de.yml +83 -0
- data/sidekiq-8.1.6/web/locales/el.yml +89 -0
- data/sidekiq-8.1.6/web/locales/en.yml +110 -0
- data/sidekiq-8.1.6/web/locales/es.yml +109 -0
- data/sidekiq-8.1.6/web/locales/fa.yml +82 -0
- data/sidekiq-8.1.6/web/locales/fr.yml +100 -0
- data/sidekiq-8.1.6/web/locales/gd.yml +110 -0
- data/sidekiq-8.1.6/web/locales/he.yml +82 -0
- data/sidekiq-8.1.6/web/locales/hi.yml +77 -0
- data/sidekiq-8.1.6/web/locales/it.yml +109 -0
- data/sidekiq-8.1.6/web/locales/ja.yml +92 -0
- data/sidekiq-8.1.6/web/locales/ko.yml +70 -0
- data/sidekiq-8.1.6/web/locales/lt.yml +85 -0
- data/sidekiq-8.1.6/web/locales/nb.yml +79 -0
- data/sidekiq-8.1.6/web/locales/nl.yml +70 -0
- data/sidekiq-8.1.6/web/locales/pl.yml +61 -0
- data/sidekiq-8.1.6/web/locales/pt-BR.yml +97 -0
- data/sidekiq-8.1.6/web/locales/pt.yml +69 -0
- data/sidekiq-8.1.6/web/locales/ru.yml +85 -0
- data/sidekiq-8.1.6/web/locales/sv.yml +70 -0
- data/sidekiq-8.1.6/web/locales/ta.yml +77 -0
- data/sidekiq-8.1.6/web/locales/tr.yml +102 -0
- data/sidekiq-8.1.6/web/locales/uk.yml +102 -0
- data/sidekiq-8.1.6/web/locales/ur.yml +82 -0
- data/sidekiq-8.1.6/web/locales/vi.yml +85 -0
- data/sidekiq-8.1.6/web/locales/zh-CN.yml +107 -0
- data/sidekiq-8.1.6/web/locales/zh-TW.yml +103 -0
- data/sidekiq-8.1.6/web/views/_footer.html.erb +32 -0
- data/sidekiq-8.1.6/web/views/_job_info.html.erb +115 -0
- data/sidekiq-8.1.6/web/views/_metrics_period_select.html.erb +15 -0
- data/sidekiq-8.1.6/web/views/_nav.html.erb +45 -0
- data/sidekiq-8.1.6/web/views/_paging.html.erb +26 -0
- data/sidekiq-8.1.6/web/views/_poll_link.html.erb +4 -0
- data/sidekiq-8.1.6/web/views/_summary.html.erb +40 -0
- data/sidekiq-8.1.6/web/views/busy.html.erb +155 -0
- data/sidekiq-8.1.6/web/views/dashboard.html.erb +104 -0
- data/sidekiq-8.1.6/web/views/dead.html.erb +38 -0
- data/sidekiq-8.1.6/web/views/filtering.html.erb +6 -0
- data/sidekiq-8.1.6/web/views/layout.html.erb +26 -0
- data/sidekiq-8.1.6/web/views/metrics.html.erb +85 -0
- data/sidekiq-8.1.6/web/views/metrics_for_job.html.erb +58 -0
- data/sidekiq-8.1.6/web/views/morgue.html.erb +69 -0
- data/sidekiq-8.1.6/web/views/profiles.html.erb +43 -0
- data/sidekiq-8.1.6/web/views/queue.html.erb +57 -0
- data/sidekiq-8.1.6/web/views/queues.html.erb +46 -0
- data/sidekiq-8.1.6/web/views/retries.html.erb +77 -0
- data/sidekiq-8.1.6/web/views/retry.html.erb +39 -0
- data/sidekiq-8.1.6/web/views/scheduled.html.erb +64 -0
- data/sidekiq-8.1.6/web/views/scheduled_job_info.html.erb +8 -0
- metadata +190 -0
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "sidekiq/manager"
|
|
4
|
+
require "sidekiq/capsule"
|
|
5
|
+
require "sidekiq/scheduled"
|
|
6
|
+
require "sidekiq/ring_buffer"
|
|
7
|
+
|
|
8
|
+
module Sidekiq
|
|
9
|
+
# The Launcher starts the Capsule Managers, the Poller thread and provides the process heartbeat.
|
|
10
|
+
class Launcher
|
|
11
|
+
include Sidekiq::Component
|
|
12
|
+
|
|
13
|
+
STATS_TTL = 5 * 365 * 24 * 60 * 60 # 5 years
|
|
14
|
+
|
|
15
|
+
PROCTITLES = [
|
|
16
|
+
proc { "sidekiq" },
|
|
17
|
+
proc { Sidekiq::VERSION },
|
|
18
|
+
proc { |me, data| data["tag"] },
|
|
19
|
+
proc { |me, data| "[#{Processor::WORK_STATE.size} of #{me.config.total_concurrency} busy]" },
|
|
20
|
+
proc { |me, data| "stopping" if me.stopping? }
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
attr_accessor :managers, :poller
|
|
24
|
+
|
|
25
|
+
def initialize(config, embedded: false)
|
|
26
|
+
@config = config
|
|
27
|
+
@embedded = embedded
|
|
28
|
+
@managers = config.capsules.values.map do |cap|
|
|
29
|
+
Sidekiq::Manager.new(cap)
|
|
30
|
+
end
|
|
31
|
+
@poller = Sidekiq::Scheduled::Poller.new(@config)
|
|
32
|
+
@done = false
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Start this Sidekiq instance. If an embedding process already
|
|
36
|
+
# has a heartbeat thread, caller can use `async_beat: false`
|
|
37
|
+
# and instead have thread call Launcher#heartbeat every N seconds.
|
|
38
|
+
def run(async_beat: true)
|
|
39
|
+
logger.debug { @config.merge!({}) }
|
|
40
|
+
Sidekiq.freeze!
|
|
41
|
+
@thread = safe_thread("heartbeat", &method(:start_heartbeat)) if async_beat
|
|
42
|
+
@poller.start
|
|
43
|
+
@managers.each(&:start)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Stops this instance from processing any more jobs,
|
|
47
|
+
def quiet
|
|
48
|
+
return if @done
|
|
49
|
+
|
|
50
|
+
@done = true
|
|
51
|
+
@managers.each(&:quiet)
|
|
52
|
+
@poller.terminate
|
|
53
|
+
fire_event(:quiet, reverse: true)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Shuts down this Sidekiq instance. Waits up to the deadline for all jobs to complete.
|
|
57
|
+
def stop
|
|
58
|
+
deadline = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC) + @config[:timeout]
|
|
59
|
+
|
|
60
|
+
quiet
|
|
61
|
+
stoppers = @managers.map do |mgr|
|
|
62
|
+
Thread.new do
|
|
63
|
+
mgr.stop(deadline)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
fire_event(:shutdown, reverse: true)
|
|
68
|
+
stoppers.each(&:join)
|
|
69
|
+
|
|
70
|
+
clear_heartbeat
|
|
71
|
+
fire_event(:exit, reverse: true)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def stopping?
|
|
75
|
+
@done
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# If embedding Sidekiq, you can have the process heartbeat
|
|
79
|
+
# call this method to regularly heartbeat rather than creating
|
|
80
|
+
# a separate thread.
|
|
81
|
+
def heartbeat
|
|
82
|
+
❤
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
private
|
|
86
|
+
|
|
87
|
+
BEAT_PAUSE = 10
|
|
88
|
+
|
|
89
|
+
def start_heartbeat
|
|
90
|
+
loop do
|
|
91
|
+
beat
|
|
92
|
+
sleep BEAT_PAUSE
|
|
93
|
+
end
|
|
94
|
+
logger.info("Heartbeat stopping...")
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def beat
|
|
98
|
+
$0 = PROCTITLES.map { |proc| proc.call(self, to_data) }.compact.join(" ") unless @embedded
|
|
99
|
+
❤
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def clear_heartbeat
|
|
103
|
+
flush_stats
|
|
104
|
+
|
|
105
|
+
# Remove record from Redis since we are shutting down.
|
|
106
|
+
# Note we don't stop the heartbeat thread; if the process
|
|
107
|
+
# doesn't actually exit, it'll reappear in the Web UI.
|
|
108
|
+
redis do |conn|
|
|
109
|
+
conn.pipelined do |pipeline|
|
|
110
|
+
pipeline.srem("processes", [identity])
|
|
111
|
+
pipeline.unlink("#{identity}:work")
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
rescue
|
|
115
|
+
# best effort, ignore network errors
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def flush_stats
|
|
119
|
+
fails = Processor::FAILURE.reset
|
|
120
|
+
procd = Processor::PROCESSED.reset
|
|
121
|
+
return if fails + procd == 0
|
|
122
|
+
|
|
123
|
+
nowdate = Time.now.utc.strftime("%Y-%m-%d")
|
|
124
|
+
begin
|
|
125
|
+
redis do |conn|
|
|
126
|
+
conn.pipelined do |pipeline|
|
|
127
|
+
pipeline.incrby("stat:processed", procd)
|
|
128
|
+
pipeline.incrby("stat:processed:#{nowdate}", procd)
|
|
129
|
+
pipeline.expire("stat:processed:#{nowdate}", STATS_TTL)
|
|
130
|
+
|
|
131
|
+
pipeline.incrby("stat:failed", fails)
|
|
132
|
+
pipeline.incrby("stat:failed:#{nowdate}", fails)
|
|
133
|
+
pipeline.expire("stat:failed:#{nowdate}", STATS_TTL)
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
rescue => ex
|
|
137
|
+
logger.warn("Unable to flush stats: #{ex}")
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def ❤
|
|
142
|
+
key = identity
|
|
143
|
+
fails = procd = 0
|
|
144
|
+
|
|
145
|
+
idle_timeout = config[:redis_idle_timeout]
|
|
146
|
+
if idle_timeout
|
|
147
|
+
config.capsules.each_value { |cap| cap.local_redis_pool.reap(idle_seconds: idle_timeout, &:close) }
|
|
148
|
+
config.local_redis_pool.reap(idle_seconds: idle_timeout, &:close)
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
begin
|
|
152
|
+
flush_stats
|
|
153
|
+
|
|
154
|
+
curstate = Processor::WORK_STATE.dup
|
|
155
|
+
curstate.transform_values! { |val| Sidekiq.dump_json(val) }
|
|
156
|
+
|
|
157
|
+
redis do |conn|
|
|
158
|
+
# work is the current set of executing jobs
|
|
159
|
+
work_key = "#{key}:work"
|
|
160
|
+
conn.multi do |transaction|
|
|
161
|
+
transaction.unlink(work_key)
|
|
162
|
+
if curstate.size > 0
|
|
163
|
+
transaction.hset(work_key, curstate)
|
|
164
|
+
transaction.expire(work_key, 60)
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
rtt = check_rtt
|
|
170
|
+
|
|
171
|
+
fails = procd = 0
|
|
172
|
+
kb = memory_usage(::Process.pid)
|
|
173
|
+
|
|
174
|
+
_, exists, _, _, signal = redis { |conn|
|
|
175
|
+
conn.multi { |transaction|
|
|
176
|
+
transaction.sadd("processes", [key])
|
|
177
|
+
transaction.exists(key)
|
|
178
|
+
transaction.hset(key, "info", to_json,
|
|
179
|
+
"concurrency", @config.total_concurrency,
|
|
180
|
+
"busy", curstate.size,
|
|
181
|
+
"beat", Time.now.to_f,
|
|
182
|
+
"rtt_us", rtt,
|
|
183
|
+
"quiet", @done.to_s,
|
|
184
|
+
"rss", kb)
|
|
185
|
+
transaction.expire(key, 60)
|
|
186
|
+
transaction.rpop("#{key}-signals")
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
# first heartbeat or recovering from an outage and need to reestablish our heartbeat
|
|
191
|
+
fire_event(:heartbeat) unless exists > 0
|
|
192
|
+
fire_event(:beat, oneshot: false)
|
|
193
|
+
|
|
194
|
+
::Process.kill(signal, ::Process.pid) if signal && !@embedded
|
|
195
|
+
rescue => e
|
|
196
|
+
# ignore all redis/network issues
|
|
197
|
+
logger.error("heartbeat: #{e}")
|
|
198
|
+
# don't lose the counts if there was a network issue
|
|
199
|
+
Processor::PROCESSED.incr(procd)
|
|
200
|
+
Processor::FAILURE.incr(fails)
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# We run the heartbeat every five seconds.
|
|
205
|
+
# Capture five samples of RTT, log a warning if each sample
|
|
206
|
+
# is above our warning threshold.
|
|
207
|
+
RTT_READINGS = RingBuffer.new(5)
|
|
208
|
+
RTT_WARNING_LEVEL = 50_000
|
|
209
|
+
|
|
210
|
+
def check_rtt
|
|
211
|
+
a = b = 0
|
|
212
|
+
redis do |x|
|
|
213
|
+
a = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC, :microsecond)
|
|
214
|
+
x.ping
|
|
215
|
+
b = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC, :microsecond)
|
|
216
|
+
end
|
|
217
|
+
rtt = b - a
|
|
218
|
+
RTT_READINGS << rtt
|
|
219
|
+
# Ideal RTT for Redis is < 1000µs
|
|
220
|
+
# Workable is < 10,000µs
|
|
221
|
+
# Log a warning if it's a disaster.
|
|
222
|
+
if RTT_READINGS.all? { |x| x > RTT_WARNING_LEVEL }
|
|
223
|
+
logger.warn <<~EOM
|
|
224
|
+
Your Redis network connection appears to be performing poorly.
|
|
225
|
+
Last RTT readings were #{RTT_READINGS.buffer.inspect}, ideally these should be < 1000.
|
|
226
|
+
Ensure Redis is running in the same AZ or datacenter as Sidekiq and that
|
|
227
|
+
your Sidekiq process is not CPU-saturated; reduce your concurrency and/or
|
|
228
|
+
see https://github.com/sidekiq/sidekiq/discussions/5039
|
|
229
|
+
EOM
|
|
230
|
+
RTT_READINGS.reset
|
|
231
|
+
end
|
|
232
|
+
rtt
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
MEMORY_GRABBER = case RUBY_PLATFORM
|
|
236
|
+
when /linux/
|
|
237
|
+
->(pid) {
|
|
238
|
+
IO.readlines("/proc/#{$$}/status").each do |line|
|
|
239
|
+
next unless line.start_with?("VmRSS:")
|
|
240
|
+
break line.split[1].to_i
|
|
241
|
+
end
|
|
242
|
+
}
|
|
243
|
+
when /darwin|bsd/
|
|
244
|
+
->(pid) {
|
|
245
|
+
`ps -o pid,rss -p #{pid}`.lines.last.split.last.to_i
|
|
246
|
+
}
|
|
247
|
+
else
|
|
248
|
+
->(pid) { 0 }
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def memory_usage(pid)
|
|
252
|
+
MEMORY_GRABBER.call(pid)
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
def to_data
|
|
256
|
+
@data ||= {
|
|
257
|
+
"hostname" => hostname,
|
|
258
|
+
"started_at" => Time.now.to_f,
|
|
259
|
+
"pid" => ::Process.pid,
|
|
260
|
+
"tag" => @config[:tag] || "",
|
|
261
|
+
"capsules" => @config.capsules.each_with_object({}) { |(name, cap), memo|
|
|
262
|
+
memo[name] = cap.to_h
|
|
263
|
+
},
|
|
264
|
+
#####
|
|
265
|
+
# TODO deprecated, remove in 9.0
|
|
266
|
+
# This data is now found in the `capsules` element above
|
|
267
|
+
"queues" => @config.capsules.values.flat_map { |cap| cap.queues }.uniq,
|
|
268
|
+
"weights" => @config.capsules.values.map(&:weights),
|
|
269
|
+
#####
|
|
270
|
+
"labels" => @config[:labels].to_a,
|
|
271
|
+
"identity" => identity,
|
|
272
|
+
"version" => Sidekiq::VERSION,
|
|
273
|
+
"embedded" => @embedded
|
|
274
|
+
}
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
def to_json
|
|
278
|
+
# this data changes infrequently so dump it to a string
|
|
279
|
+
# now so we don't need to dump it every heartbeat.
|
|
280
|
+
@json ||= Sidekiq.dump_json(to_data)
|
|
281
|
+
end
|
|
282
|
+
end
|
|
283
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
module Sidekiq
|
|
2
|
+
require "sidekiq/component"
|
|
3
|
+
|
|
4
|
+
class Loader
|
|
5
|
+
include Sidekiq::Component
|
|
6
|
+
|
|
7
|
+
def initialize(cfg = Sidekiq.default_configuration)
|
|
8
|
+
@config = cfg
|
|
9
|
+
@load_hooks = Hash.new { |h, k| h[k] = [] }
|
|
10
|
+
@loaded = Set.new
|
|
11
|
+
@lock = Mutex.new
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Declares a block that will be executed when a Sidekiq component is fully
|
|
15
|
+
# loaded. If the component has already loaded, the block is executed
|
|
16
|
+
# immediately.
|
|
17
|
+
#
|
|
18
|
+
# Sidekiq.loader.on_load(:api) do
|
|
19
|
+
# # extend the sidekiq API
|
|
20
|
+
# end
|
|
21
|
+
#
|
|
22
|
+
def on_load(name, &block)
|
|
23
|
+
# we don't want to hold the lock while calling the block
|
|
24
|
+
to_run = nil
|
|
25
|
+
|
|
26
|
+
@lock.synchronize do
|
|
27
|
+
if @loaded.include?(name)
|
|
28
|
+
to_run = block
|
|
29
|
+
else
|
|
30
|
+
@load_hooks[name] << block
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
to_run&.call
|
|
35
|
+
nil
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Executes all blocks registered to +name+ via on_load.
|
|
39
|
+
#
|
|
40
|
+
# Sidekiq.loader.run_load_hooks(:api)
|
|
41
|
+
#
|
|
42
|
+
# In the case of the above example, it will execute all hooks registered for +:api+.
|
|
43
|
+
#
|
|
44
|
+
def run_load_hooks(name)
|
|
45
|
+
hks = @lock.synchronize do
|
|
46
|
+
@loaded << name
|
|
47
|
+
@load_hooks.delete(name)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
hks&.each do |blk|
|
|
51
|
+
blk.call
|
|
52
|
+
rescue => ex
|
|
53
|
+
handle_exception(ex, hook: name)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "logger"
|
|
4
|
+
require "time"
|
|
5
|
+
|
|
6
|
+
module Sidekiq
|
|
7
|
+
module Context
|
|
8
|
+
def self.with(hash)
|
|
9
|
+
orig_context = current.dup
|
|
10
|
+
current.merge!(hash)
|
|
11
|
+
yield
|
|
12
|
+
ensure
|
|
13
|
+
Thread.current[:sidekiq_context] = orig_context
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def self.current
|
|
17
|
+
Thread.current[:sidekiq_context] ||= {}
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def self.add(k, v)
|
|
21
|
+
current[k] = v
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
class Logger < ::Logger
|
|
26
|
+
module Formatters
|
|
27
|
+
class Base < ::Logger::Formatter
|
|
28
|
+
COLORS = {
|
|
29
|
+
"DEBUG" => "\e[1;32mDEBUG\e[0m", # green
|
|
30
|
+
"INFO" => "\e[1;34mINFO \e[0m", # blue
|
|
31
|
+
"WARN" => "\e[1;33mWARN \e[0m", # yellow
|
|
32
|
+
"ERROR" => "\e[1;31mERROR\e[0m", # red
|
|
33
|
+
"FATAL" => "\e[1;35mFATAL\e[0m" # pink
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
def tid
|
|
37
|
+
Thread.current["sidekiq_tid"] ||= (Thread.current.object_id ^ ::Process.pid).to_s(36)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def format_context(ctxt = Sidekiq::Context.current)
|
|
41
|
+
(ctxt.size == 0) ? "" : " #{ctxt.map { |k, v|
|
|
42
|
+
case v
|
|
43
|
+
when Array
|
|
44
|
+
"#{k}=#{v.join(",")}"
|
|
45
|
+
else
|
|
46
|
+
"#{k}=#{v}"
|
|
47
|
+
end
|
|
48
|
+
}.join(" ")}"
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
class Pretty < Base
|
|
53
|
+
def call(severity, time, program_name, message)
|
|
54
|
+
"#{COLORS[severity]} #{time.utc.iso8601(3)} pid=#{::Process.pid} tid=#{tid}#{format_context}: #{message}\n"
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
class Plain < Base
|
|
59
|
+
def call(severity, time, program_name, message)
|
|
60
|
+
"#{severity} #{time.utc.iso8601(3)} pid=#{::Process.pid} tid=#{tid}#{format_context}: #{message}\n"
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
class WithoutTimestamp < Pretty
|
|
65
|
+
def call(severity, time, program_name, message)
|
|
66
|
+
"#{COLORS[severity]} pid=#{::Process.pid} tid=#{tid}#{format_context}: #{message}\n"
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
class JSON < Base
|
|
71
|
+
def call(severity, time, program_name, message)
|
|
72
|
+
hash = {
|
|
73
|
+
ts: time.utc.iso8601(3),
|
|
74
|
+
pid: ::Process.pid,
|
|
75
|
+
tid: tid,
|
|
76
|
+
lvl: severity,
|
|
77
|
+
msg: message
|
|
78
|
+
}
|
|
79
|
+
c = Sidekiq::Context.current
|
|
80
|
+
hash["ctx"] = c unless c.empty?
|
|
81
|
+
|
|
82
|
+
Sidekiq.dump_json(hash) << "\n"
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "sidekiq/processor"
|
|
4
|
+
|
|
5
|
+
module Sidekiq
|
|
6
|
+
##
|
|
7
|
+
# The Manager is the central coordination point in Sidekiq, controlling
|
|
8
|
+
# the lifecycle of the Processors.
|
|
9
|
+
#
|
|
10
|
+
# Tasks:
|
|
11
|
+
#
|
|
12
|
+
# 1. start: Spin up Processors.
|
|
13
|
+
# 3. processor_died: Handle job failure, throw away Processor, create new one.
|
|
14
|
+
# 4. quiet: shutdown idle Processors.
|
|
15
|
+
# 5. stop: hard stop the Processors by deadline.
|
|
16
|
+
#
|
|
17
|
+
# Note that only the last task requires its own Thread since it has to monitor
|
|
18
|
+
# the shutdown process. The other tasks are performed by other threads.
|
|
19
|
+
#
|
|
20
|
+
class Manager
|
|
21
|
+
include Sidekiq::Component
|
|
22
|
+
|
|
23
|
+
attr_reader :workers
|
|
24
|
+
attr_reader :capsule
|
|
25
|
+
|
|
26
|
+
def initialize(capsule)
|
|
27
|
+
@config = @capsule = capsule
|
|
28
|
+
@count = capsule.concurrency
|
|
29
|
+
raise ArgumentError, "Concurrency of #{@count} is not supported" if @count < 1
|
|
30
|
+
|
|
31
|
+
@done = false
|
|
32
|
+
@workers = Set.new
|
|
33
|
+
@plock = Mutex.new
|
|
34
|
+
@count.times do
|
|
35
|
+
@workers << Processor.new(@config, &method(:processor_result))
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def start
|
|
40
|
+
@workers.each(&:start)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def quiet
|
|
44
|
+
return if @done
|
|
45
|
+
@done = true
|
|
46
|
+
|
|
47
|
+
logger.info { "Terminating quiet threads for #{capsule.name} capsule" }
|
|
48
|
+
@workers.each(&:terminate)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def stop(deadline)
|
|
52
|
+
quiet
|
|
53
|
+
|
|
54
|
+
# some of the shutdown events can be async,
|
|
55
|
+
# we don't have any way to know when they're done but
|
|
56
|
+
# give them a little time to take effect
|
|
57
|
+
sleep PAUSE_TIME
|
|
58
|
+
return if @workers.empty?
|
|
59
|
+
|
|
60
|
+
logger.info { "Pausing to allow jobs to finish..." }
|
|
61
|
+
wait_for(deadline) { @workers.empty? }
|
|
62
|
+
return if @workers.empty?
|
|
63
|
+
|
|
64
|
+
hard_shutdown
|
|
65
|
+
ensure
|
|
66
|
+
capsule.stop
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def processor_result(processor, reason = nil)
|
|
70
|
+
@plock.synchronize do
|
|
71
|
+
@workers.delete(processor)
|
|
72
|
+
if !@done && @count > @workers.size
|
|
73
|
+
p = Processor.new(@config, &method(:processor_result))
|
|
74
|
+
@workers << p
|
|
75
|
+
p.start
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
nil
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def stopped?
|
|
82
|
+
@done
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
private
|
|
86
|
+
|
|
87
|
+
def hard_shutdown
|
|
88
|
+
# We've reached the timeout and we still have busy threads.
|
|
89
|
+
# They must die but their jobs shall live on.
|
|
90
|
+
cleanup = nil
|
|
91
|
+
@plock.synchronize do
|
|
92
|
+
cleanup = @workers.dup
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
if cleanup.size > 0
|
|
96
|
+
jobs = cleanup.map { |p| p.job }.compact
|
|
97
|
+
|
|
98
|
+
logger.warn { "Terminating #{cleanup.size} busy threads" }
|
|
99
|
+
logger.debug { "Jobs still in progress #{jobs.inspect}" }
|
|
100
|
+
|
|
101
|
+
# Re-enqueue unfinished jobs
|
|
102
|
+
# NOTE: You may notice that we may push a job back to redis before
|
|
103
|
+
# the thread is terminated. This is ok because Sidekiq's
|
|
104
|
+
# contract says that jobs are run AT LEAST once. Process termination
|
|
105
|
+
# is delayed until we're certain the jobs are back in Redis because
|
|
106
|
+
# it is worse to lose a job than to run it twice.
|
|
107
|
+
capsule.fetcher.bulk_requeue(jobs)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
cleanup.each do |processor|
|
|
111
|
+
processor.kill
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# when this method returns, we immediately call `exit` which may not give
|
|
115
|
+
# the remaining threads time to run `ensure` blocks, etc. We pause here up
|
|
116
|
+
# to 3 seconds to give threads a minimal amount of time to run `ensure` blocks.
|
|
117
|
+
deadline = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC) + 3
|
|
118
|
+
wait_for(deadline) { @workers.empty? }
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# hack for quicker development / testing environment #2774
|
|
122
|
+
PAUSE_TIME = $stdout.tty? ? 0.1 : 0.5
|
|
123
|
+
|
|
124
|
+
# Wait for the orblock to be true or the deadline passed.
|
|
125
|
+
def wait_for(deadline, &condblock)
|
|
126
|
+
remaining = deadline - ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
|
|
127
|
+
while remaining > PAUSE_TIME
|
|
128
|
+
return if condblock.call
|
|
129
|
+
sleep PAUSE_TIME
|
|
130
|
+
remaining = deadline - ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|