sidekiq 5.2.7 → 8.0.5

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.
Files changed (169) hide show
  1. checksums.yaml +4 -4
  2. data/Changes.md +845 -8
  3. data/LICENSE.txt +9 -0
  4. data/README.md +54 -54
  5. data/bin/multi_queue_bench +271 -0
  6. data/bin/sidekiq +22 -3
  7. data/bin/sidekiqload +219 -112
  8. data/bin/sidekiqmon +11 -0
  9. data/bin/webload +69 -0
  10. data/lib/active_job/queue_adapters/sidekiq_adapter.rb +120 -0
  11. data/lib/generators/sidekiq/job_generator.rb +59 -0
  12. data/lib/generators/sidekiq/templates/{worker.rb.erb → job.rb.erb} +2 -2
  13. data/lib/generators/sidekiq/templates/{worker_spec.rb.erb → job_spec.rb.erb} +1 -1
  14. data/lib/generators/sidekiq/templates/{worker_test.rb.erb → job_test.rb.erb} +1 -1
  15. data/lib/sidekiq/api.rb +757 -373
  16. data/lib/sidekiq/capsule.rb +132 -0
  17. data/lib/sidekiq/cli.rb +210 -233
  18. data/lib/sidekiq/client.rb +145 -103
  19. data/lib/sidekiq/component.rb +128 -0
  20. data/lib/sidekiq/config.rb +315 -0
  21. data/lib/sidekiq/deploy.rb +64 -0
  22. data/lib/sidekiq/embedded.rb +64 -0
  23. data/lib/sidekiq/fetch.rb +49 -42
  24. data/lib/sidekiq/iterable_job.rb +56 -0
  25. data/lib/sidekiq/job/interrupt_handler.rb +24 -0
  26. data/lib/sidekiq/job/iterable/active_record_enumerator.rb +53 -0
  27. data/lib/sidekiq/job/iterable/csv_enumerator.rb +47 -0
  28. data/lib/sidekiq/job/iterable/enumerators.rb +135 -0
  29. data/lib/sidekiq/job/iterable.rb +306 -0
  30. data/lib/sidekiq/job.rb +385 -0
  31. data/lib/sidekiq/job_logger.rb +34 -7
  32. data/lib/sidekiq/job_retry.rb +164 -109
  33. data/lib/sidekiq/job_util.rb +113 -0
  34. data/lib/sidekiq/launcher.rb +208 -107
  35. data/lib/sidekiq/logger.rb +80 -0
  36. data/lib/sidekiq/manager.rb +42 -46
  37. data/lib/sidekiq/metrics/query.rb +184 -0
  38. data/lib/sidekiq/metrics/shared.rb +109 -0
  39. data/lib/sidekiq/metrics/tracking.rb +150 -0
  40. data/lib/sidekiq/middleware/chain.rb +113 -56
  41. data/lib/sidekiq/middleware/current_attributes.rb +119 -0
  42. data/lib/sidekiq/middleware/i18n.rb +7 -7
  43. data/lib/sidekiq/middleware/modules.rb +23 -0
  44. data/lib/sidekiq/monitor.rb +147 -0
  45. data/lib/sidekiq/paginator.rb +41 -16
  46. data/lib/sidekiq/processor.rb +146 -127
  47. data/lib/sidekiq/profiler.rb +72 -0
  48. data/lib/sidekiq/rails.rb +46 -43
  49. data/lib/sidekiq/redis_client_adapter.rb +113 -0
  50. data/lib/sidekiq/redis_connection.rb +79 -108
  51. data/lib/sidekiq/ring_buffer.rb +31 -0
  52. data/lib/sidekiq/scheduled.rb +112 -50
  53. data/lib/sidekiq/sd_notify.rb +149 -0
  54. data/lib/sidekiq/systemd.rb +26 -0
  55. data/lib/sidekiq/testing/inline.rb +6 -5
  56. data/lib/sidekiq/testing.rb +91 -90
  57. data/lib/sidekiq/transaction_aware_client.rb +51 -0
  58. data/lib/sidekiq/version.rb +7 -1
  59. data/lib/sidekiq/web/action.rb +125 -60
  60. data/lib/sidekiq/web/application.rb +363 -259
  61. data/lib/sidekiq/web/config.rb +120 -0
  62. data/lib/sidekiq/web/csrf_protection.rb +183 -0
  63. data/lib/sidekiq/web/helpers.rb +241 -120
  64. data/lib/sidekiq/web/router.rb +62 -71
  65. data/lib/sidekiq/web.rb +69 -161
  66. data/lib/sidekiq/worker_compatibility_alias.rb +13 -0
  67. data/lib/sidekiq.rb +94 -182
  68. data/sidekiq.gemspec +26 -16
  69. data/web/assets/images/apple-touch-icon.png +0 -0
  70. data/web/assets/javascripts/application.js +150 -61
  71. data/web/assets/javascripts/base-charts.js +120 -0
  72. data/web/assets/javascripts/chart.min.js +13 -0
  73. data/web/assets/javascripts/chartjs-adapter-date-fns.min.js +7 -0
  74. data/web/assets/javascripts/chartjs-plugin-annotation.min.js +7 -0
  75. data/web/assets/javascripts/dashboard-charts.js +194 -0
  76. data/web/assets/javascripts/dashboard.js +41 -293
  77. data/web/assets/javascripts/metrics.js +280 -0
  78. data/web/assets/stylesheets/style.css +766 -0
  79. data/web/locales/ar.yml +72 -65
  80. data/web/locales/cs.yml +63 -62
  81. data/web/locales/da.yml +61 -53
  82. data/web/locales/de.yml +66 -53
  83. data/web/locales/el.yml +44 -24
  84. data/web/locales/en.yml +94 -66
  85. data/web/locales/es.yml +92 -54
  86. data/web/locales/fa.yml +66 -65
  87. data/web/locales/fr.yml +83 -62
  88. data/web/locales/gd.yml +99 -0
  89. data/web/locales/he.yml +66 -64
  90. data/web/locales/hi.yml +60 -59
  91. data/web/locales/it.yml +93 -54
  92. data/web/locales/ja.yml +75 -64
  93. data/web/locales/ko.yml +53 -52
  94. data/web/locales/lt.yml +84 -0
  95. data/web/locales/nb.yml +62 -61
  96. data/web/locales/nl.yml +53 -52
  97. data/web/locales/pl.yml +46 -45
  98. data/web/locales/{pt-br.yml → pt-BR.yml} +84 -56
  99. data/web/locales/pt.yml +52 -51
  100. data/web/locales/ru.yml +69 -63
  101. data/web/locales/sv.yml +54 -53
  102. data/web/locales/ta.yml +61 -60
  103. data/web/locales/tr.yml +101 -0
  104. data/web/locales/uk.yml +86 -61
  105. data/web/locales/ur.yml +65 -64
  106. data/web/locales/vi.yml +84 -0
  107. data/web/locales/zh-CN.yml +106 -0
  108. data/web/locales/{zh-tw.yml → zh-TW.yml} +43 -9
  109. data/web/views/_footer.erb +31 -19
  110. data/web/views/_job_info.erb +94 -75
  111. data/web/views/_metrics_period_select.erb +15 -0
  112. data/web/views/_nav.erb +14 -21
  113. data/web/views/_paging.erb +23 -19
  114. data/web/views/_poll_link.erb +3 -6
  115. data/web/views/_summary.erb +23 -23
  116. data/web/views/busy.erb +139 -87
  117. data/web/views/dashboard.erb +82 -53
  118. data/web/views/dead.erb +31 -27
  119. data/web/views/filtering.erb +6 -0
  120. data/web/views/layout.erb +15 -29
  121. data/web/views/metrics.erb +84 -0
  122. data/web/views/metrics_for_job.erb +58 -0
  123. data/web/views/morgue.erb +60 -70
  124. data/web/views/profiles.erb +43 -0
  125. data/web/views/queue.erb +50 -39
  126. data/web/views/queues.erb +45 -29
  127. data/web/views/retries.erb +65 -75
  128. data/web/views/retry.erb +32 -27
  129. data/web/views/scheduled.erb +58 -52
  130. data/web/views/scheduled_job_info.erb +1 -1
  131. metadata +96 -76
  132. data/.circleci/config.yml +0 -61
  133. data/.github/contributing.md +0 -32
  134. data/.github/issue_template.md +0 -11
  135. data/.gitignore +0 -15
  136. data/.travis.yml +0 -11
  137. data/3.0-Upgrade.md +0 -70
  138. data/4.0-Upgrade.md +0 -53
  139. data/5.0-Upgrade.md +0 -56
  140. data/COMM-LICENSE +0 -97
  141. data/Ent-Changes.md +0 -238
  142. data/Gemfile +0 -23
  143. data/LICENSE +0 -9
  144. data/Pro-2.0-Upgrade.md +0 -138
  145. data/Pro-3.0-Upgrade.md +0 -44
  146. data/Pro-4.0-Upgrade.md +0 -35
  147. data/Pro-Changes.md +0 -759
  148. data/Rakefile +0 -9
  149. data/bin/sidekiqctl +0 -20
  150. data/code_of_conduct.md +0 -50
  151. data/lib/generators/sidekiq/worker_generator.rb +0 -49
  152. data/lib/sidekiq/core_ext.rb +0 -1
  153. data/lib/sidekiq/ctl.rb +0 -221
  154. data/lib/sidekiq/delay.rb +0 -42
  155. data/lib/sidekiq/exception_handler.rb +0 -29
  156. data/lib/sidekiq/extensions/action_mailer.rb +0 -57
  157. data/lib/sidekiq/extensions/active_record.rb +0 -40
  158. data/lib/sidekiq/extensions/class_methods.rb +0 -40
  159. data/lib/sidekiq/extensions/generic_proxy.rb +0 -31
  160. data/lib/sidekiq/logging.rb +0 -122
  161. data/lib/sidekiq/middleware/server/active_record.rb +0 -23
  162. data/lib/sidekiq/util.rb +0 -66
  163. data/lib/sidekiq/worker.rb +0 -220
  164. data/web/assets/stylesheets/application-rtl.css +0 -246
  165. data/web/assets/stylesheets/application.css +0 -1144
  166. data/web/assets/stylesheets/bootstrap-rtl.min.css +0 -9
  167. data/web/assets/stylesheets/bootstrap.css +0 -5
  168. data/web/locales/zh-cn.yml +0 -68
  169. data/web/views/_status.erb +0 -4
@@ -1,173 +1,274 @@
1
1
  # frozen_string_literal: true
2
- require 'sidekiq/manager'
3
- require 'sidekiq/fetch'
4
- require 'sidekiq/scheduled'
2
+
3
+ require "sidekiq/manager"
4
+ require "sidekiq/capsule"
5
+ require "sidekiq/scheduled"
6
+ require "sidekiq/ring_buffer"
5
7
 
6
8
  module Sidekiq
7
- # The Launcher is a very simple Actor whose job is to
8
- # start, monitor and stop the core Actors in Sidekiq.
9
- # If any of these actors die, the Sidekiq process exits
10
- # immediately.
9
+ # The Launcher starts the Capsule Managers, the Poller thread and provides the process heartbeat.
11
10
  class Launcher
12
- include Util
11
+ include Sidekiq::Component
12
+
13
+ STATS_TTL = 5 * 365 * 24 * 60 * 60 # 5 years
13
14
 
14
- attr_accessor :manager, :poller, :fetcher
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
+ ]
15
22
 
16
- STATS_TTL = 5*365*24*60*60
23
+ attr_accessor :managers, :poller
17
24
 
18
- def initialize(options)
19
- @manager = Sidekiq::Manager.new(options)
20
- @poller = Sidekiq::Scheduled::Poller.new
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)
21
32
  @done = false
22
- @options = options
23
33
  end
24
34
 
25
- def run
26
- @thread = safe_thread("heartbeat", &method(:start_heartbeat))
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
27
42
  @poller.start
28
- @manager.start
43
+ @managers.each(&:start)
29
44
  end
30
45
 
31
46
  # Stops this instance from processing any more jobs,
32
- #
33
47
  def quiet
48
+ return if @done
49
+
34
50
  @done = true
35
- @manager.quiet
51
+ @managers.each(&:quiet)
36
52
  @poller.terminate
53
+ fire_event(:quiet, reverse: true)
37
54
  end
38
55
 
39
- # Shuts down the process. This method does not
40
- # return until all work is complete and cleaned up.
41
- # It can take up to the timeout to complete.
56
+ # Shuts down this Sidekiq instance. Waits up to the deadline for all jobs to complete.
42
57
  def stop
43
- deadline = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC) + @options[:timeout]
58
+ deadline = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC) + @config[:timeout]
44
59
 
45
- @done = true
46
- @manager.quiet
47
- @poller.terminate
48
-
49
- @manager.stop(deadline)
60
+ quiet
61
+ stoppers = @managers.map do |mgr|
62
+ Thread.new do
63
+ mgr.stop(deadline)
64
+ end
65
+ end
50
66
 
51
- # Requeue everything in case there was a worker who grabbed work while stopped
52
- # This call is a no-op in Sidekiq but necessary for Sidekiq Pro.
53
- strategy = (@options[:fetch] || Sidekiq::BasicFetch)
54
- strategy.bulk_requeue([], @options)
67
+ fire_event(:shutdown, reverse: true)
68
+ stoppers.each(&:join)
55
69
 
56
70
  clear_heartbeat
71
+ fire_event(:exit, reverse: true)
57
72
  end
58
73
 
59
74
  def stopping?
60
75
  @done
61
76
  end
62
77
 
63
- private unless $TESTING
64
-
78
+ # If embedding Sidekiq, you can have the process heartbeat
79
+ # call this method to regularly heartbeat rather than creating
80
+ # a separate thread.
65
81
  def heartbeat
66
- results = Sidekiq::CLI::PROCTITLES.map {|x| x.(self, to_data) }
67
- results.compact!
68
- $0 = results.join(' ')
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
69
96
 
97
+ def beat
98
+ $0 = PROCTITLES.map { |proc| proc.call(self, to_data) }.compact.join(" ") unless @embedded
70
99
 
71
100
  end
72
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
+
73
141
  def ❤
74
142
  key = identity
75
143
  fails = procd = 0
144
+
76
145
  begin
77
- fails = Processor::FAILURE.reset
78
- procd = Processor::PROCESSED.reset
79
- curstate = Processor::WORKER_STATE.dup
80
-
81
- workers_key = "#{key}:workers"
82
- nowdate = Time.now.utc.strftime("%Y-%m-%d")
83
- Sidekiq.redis do |conn|
84
- conn.multi do
85
- conn.incrby("stat:processed", procd)
86
- conn.incrby("stat:processed:#{nowdate}", procd)
87
- conn.expire("stat:processed:#{nowdate}", STATS_TTL)
88
-
89
- conn.incrby("stat:failed", fails)
90
- conn.incrby("stat:failed:#{nowdate}", fails)
91
- conn.expire("stat:failed:#{nowdate}", STATS_TTL)
92
-
93
- conn.del(workers_key)
94
- curstate.each_pair do |tid, hash|
95
- conn.hset(workers_key, tid, Sidekiq.dump_json(hash))
146
+ flush_stats
147
+
148
+ curstate = Processor::WORK_STATE.dup
149
+ curstate.transform_values! { |val| Sidekiq.dump_json(val) }
150
+
151
+ redis do |conn|
152
+ # work is the current set of executing jobs
153
+ work_key = "#{key}:work"
154
+ conn.multi do |transaction|
155
+ transaction.unlink(work_key)
156
+ if curstate.size > 0
157
+ transaction.hset(work_key, curstate)
158
+ transaction.expire(work_key, 60)
96
159
  end
97
- conn.expire(workers_key, 60)
98
160
  end
99
161
  end
162
+
163
+ rtt = check_rtt
164
+
100
165
  fails = procd = 0
166
+ kb = memory_usage(::Process.pid)
101
167
 
102
- _, exists, _, _, msg = Sidekiq.redis do |conn|
103
- conn.multi do
104
- conn.sadd('processes', key)
105
- conn.exists(key)
106
- conn.hmset(key, 'info', to_json, 'busy', curstate.size, 'beat', Time.now.to_f, 'quiet', @done)
107
- conn.expire(key, 60)
108
- conn.rpop("#{key}-signals")
109
- end
110
- end
168
+ _, exists, _, _, signal = redis { |conn|
169
+ conn.multi { |transaction|
170
+ transaction.sadd("processes", [key])
171
+ transaction.exists(key)
172
+ transaction.hset(key, "info", to_json,
173
+ "busy", curstate.size,
174
+ "beat", Time.now.to_f,
175
+ "rtt_us", rtt,
176
+ "quiet", @done.to_s,
177
+ "rss", kb)
178
+ transaction.expire(key, 60)
179
+ transaction.rpop("#{key}-signals")
180
+ }
181
+ }
111
182
 
112
183
  # first heartbeat or recovering from an outage and need to reestablish our heartbeat
113
- fire_event(:heartbeat) if !exists
184
+ fire_event(:heartbeat) unless exists > 0
185
+ fire_event(:beat, oneshot: false)
114
186
 
115
- return unless msg
116
-
117
- ::Process.kill(msg, $$)
187
+ ::Process.kill(signal, ::Process.pid) if signal && !@embedded
118
188
  rescue => e
119
189
  # ignore all redis/network issues
120
- logger.error("heartbeat: #{e.message}")
190
+ logger.error("heartbeat: #{e}")
121
191
  # don't lose the counts if there was a network issue
122
192
  Processor::PROCESSED.incr(procd)
123
193
  Processor::FAILURE.incr(fails)
124
194
  end
125
195
  end
126
196
 
127
- def start_heartbeat
128
- while true
129
- heartbeat
130
- sleep 5
197
+ # We run the heartbeat every five seconds.
198
+ # Capture five samples of RTT, log a warning if each sample
199
+ # is above our warning threshold.
200
+ RTT_READINGS = RingBuffer.new(5)
201
+ RTT_WARNING_LEVEL = 50_000
202
+
203
+ def check_rtt
204
+ a = b = 0
205
+ redis do |x|
206
+ a = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC, :microsecond)
207
+ x.ping
208
+ b = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC, :microsecond)
209
+ end
210
+ rtt = b - a
211
+ RTT_READINGS << rtt
212
+ # Ideal RTT for Redis is < 1000µs
213
+ # Workable is < 10,000µs
214
+ # Log a warning if it's a disaster.
215
+ if RTT_READINGS.all? { |x| x > RTT_WARNING_LEVEL }
216
+ logger.warn <<~EOM
217
+ Your Redis network connection is performing extremely poorly.
218
+ Last RTT readings were #{RTT_READINGS.buffer.inspect}, ideally these should be < 1000.
219
+ Ensure Redis is running in the same AZ or datacenter as Sidekiq.
220
+ If these values are close to 100,000, that means your Sidekiq process may be
221
+ CPU-saturated; reduce your concurrency and/or see https://github.com/sidekiq/sidekiq/discussions/5039
222
+ EOM
223
+ RTT_READINGS.reset
131
224
  end
132
- Sidekiq.logger.info("Heartbeat stopping...")
225
+ rtt
133
226
  end
134
227
 
135
- def to_data
136
- @data ||= begin
137
- {
138
- 'hostname' => hostname,
139
- 'started_at' => Time.now.to_f,
140
- 'pid' => $$,
141
- 'tag' => @options[:tag] || '',
142
- 'concurrency' => @options[:concurrency],
143
- 'queues' => @options[:queues].uniq,
144
- 'labels' => @options[:labels],
145
- 'identity' => identity,
146
- }
147
- end
228
+ MEMORY_GRABBER = case RUBY_PLATFORM
229
+ when /linux/
230
+ ->(pid) {
231
+ IO.readlines("/proc/#{$$}/status").each do |line|
232
+ next unless line.start_with?("VmRSS:")
233
+ break line.split[1].to_i
234
+ end
235
+ }
236
+ when /darwin|bsd/
237
+ ->(pid) {
238
+ `ps -o pid,rss -p #{pid}`.lines.last.split.last.to_i
239
+ }
240
+ else
241
+ ->(pid) { 0 }
148
242
  end
149
243
 
150
- def to_json
151
- @json ||= begin
152
- # this data changes infrequently so dump it to a string
153
- # now so we don't need to dump it every heartbeat.
154
- Sidekiq.dump_json(to_data)
155
- end
244
+ def memory_usage(pid)
245
+ MEMORY_GRABBER.call(pid)
156
246
  end
157
247
 
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
248
+ def to_data
249
+ @data ||= {
250
+ "hostname" => hostname,
251
+ "started_at" => Time.now.to_f,
252
+ "pid" => ::Process.pid,
253
+ "tag" => @config[:tag] || "",
254
+ "concurrency" => @config.total_concurrency,
255
+ "queues" => @config.capsules.values.flat_map { |cap| cap.queues }.uniq,
256
+ "weights" => to_weights,
257
+ "labels" => @config[:labels].to_a,
258
+ "identity" => identity,
259
+ "version" => Sidekiq::VERSION,
260
+ "embedded" => @embedded
261
+ }
170
262
  end
171
263
 
264
+ def to_weights
265
+ @config.capsules.values.map(&:weights)
266
+ end
267
+
268
+ def to_json
269
+ # this data changes infrequently so dump it to a string
270
+ # now so we don't need to dump it every heartbeat.
271
+ @json ||= Sidekiq.dump_json(to_data)
272
+ end
172
273
  end
173
274
  end
@@ -0,0 +1,80 @@
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
+ COLORS = {
28
+ "DEBUG" => "\e[1;32mDEBUG\e[0m", # green
29
+ "INFO" => "\e[1;34mINFO \e[0m", # blue
30
+ "WARN" => "\e[1;33mWARN \e[0m", # yellow
31
+ "ERROR" => "\e[1;31mERROR\e[0m", # red
32
+ "FATAL" => "\e[1;35mFATAL\e[0m" # pink
33
+ }
34
+ class Base < ::Logger::Formatter
35
+ def tid
36
+ Thread.current["sidekiq_tid"] ||= (Thread.current.object_id ^ ::Process.pid).to_s(36)
37
+ end
38
+
39
+ def format_context(ctxt = Sidekiq::Context.current)
40
+ (ctxt.size == 0) ? "" : " #{ctxt.map { |k, v|
41
+ case v
42
+ when Array
43
+ "#{k}=#{v.join(",")}"
44
+ else
45
+ "#{k}=#{v}"
46
+ end
47
+ }.join(" ")}"
48
+ end
49
+ end
50
+
51
+ class Pretty < Base
52
+ def call(severity, time, program_name, message)
53
+ "#{Formatters::COLORS[severity]} #{time.utc.iso8601(3)} pid=#{::Process.pid} tid=#{tid}#{format_context}: #{message}\n"
54
+ end
55
+ end
56
+
57
+ class WithoutTimestamp < Pretty
58
+ def call(severity, time, program_name, message)
59
+ "#{Formatters::COLORS[severity]} pid=#{::Process.pid} tid=#{tid} #{format_context}: #{message}\n"
60
+ end
61
+ end
62
+
63
+ class JSON < Base
64
+ def call(severity, time, program_name, message)
65
+ hash = {
66
+ ts: time.utc.iso8601(3),
67
+ pid: ::Process.pid,
68
+ tid: tid,
69
+ lvl: severity,
70
+ msg: message
71
+ }
72
+ c = Sidekiq::Context.current
73
+ hash["ctx"] = c unless c.empty?
74
+
75
+ Sidekiq.dump_json(hash) << "\n"
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
@@ -1,12 +1,8 @@
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
- module Sidekiq
3
+ require "sidekiq/processor"
9
4
 
5
+ module Sidekiq
10
6
  ##
11
7
  # The Manager is the central coordination point in Sidekiq, controlling
12
8
  # the lifecycle of the Processors.
@@ -22,46 +18,38 @@ module Sidekiq
22
18
  # the shutdown process. The other tasks are performed by other threads.
23
19
  #
24
20
  class Manager
25
- include Util
21
+ include Sidekiq::Component
26
22
 
27
23
  attr_reader :workers
28
- attr_reader :options
24
+ attr_reader :capsule
29
25
 
30
- def initialize(options={})
31
- logger.debug { options.inspect }
32
- @options = options
33
- @count = options[:concurrency] || 10
26
+ def initialize(capsule)
27
+ @config = @capsule = capsule
28
+ @count = capsule.concurrency
34
29
  raise ArgumentError, "Concurrency of #{@count} is not supported" if @count < 1
35
30
 
36
31
  @done = false
37
32
  @workers = Set.new
33
+ @plock = Mutex.new
38
34
  @count.times do
39
- @workers << Processor.new(self)
35
+ @workers << Processor.new(@config, &method(:processor_result))
40
36
  end
41
- @plock = Mutex.new
42
37
  end
43
38
 
44
39
  def start
45
- @workers.each do |x|
46
- x.start
47
- end
40
+ @workers.each(&:start)
48
41
  end
49
42
 
50
43
  def quiet
51
44
  return if @done
52
45
  @done = true
53
46
 
54
- logger.info { "Terminating quiet workers" }
55
- @workers.each { |x| x.terminate }
56
- fire_event(:quiet, reverse: true)
47
+ logger.info { "Terminating quiet threads for #{capsule.name} capsule" }
48
+ @workers.each(&:terminate)
57
49
  end
58
50
 
59
- # hack for quicker development / testing environment #2774
60
- PAUSE_TIME = STDOUT.tty? ? 0.1 : 0.5
61
-
62
51
  def stop(deadline)
63
52
  quiet
64
- fire_event(:shutdown, reverse: true)
65
53
 
66
54
  # some of the shutdown events can be async,
67
55
  # we don't have any way to know when they're done but
@@ -69,29 +57,20 @@ module Sidekiq
69
57
  sleep PAUSE_TIME
70
58
  return if @workers.empty?
71
59
 
72
- logger.info { "Pausing to allow workers to finish..." }
73
- remaining = deadline - ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
74
- while remaining > PAUSE_TIME
75
- return if @workers.empty?
76
- sleep PAUSE_TIME
77
- remaining = deadline - ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
78
- end
60
+ logger.info { "Pausing to allow jobs to finish..." }
61
+ wait_for(deadline) { @workers.empty? }
79
62
  return if @workers.empty?
80
63
 
81
64
  hard_shutdown
65
+ ensure
66
+ capsule.stop
82
67
  end
83
68
 
84
- def processor_stopped(processor)
85
- @plock.synchronize do
86
- @workers.delete(processor)
87
- end
88
- end
89
-
90
- def processor_died(processor, reason)
69
+ def processor_result(processor, reason = nil)
91
70
  @plock.synchronize do
92
71
  @workers.delete(processor)
93
72
  unless @done
94
- p = Processor.new(self)
73
+ p = Processor.new(@config, &method(:processor_result))
95
74
  @workers << p
96
75
  p.start
97
76
  end
@@ -105,7 +84,7 @@ module Sidekiq
105
84
  private
106
85
 
107
86
  def hard_shutdown
108
- # We've reached the timeout and we still have busy workers.
87
+ # We've reached the timeout and we still have busy threads.
109
88
  # They must die but their jobs shall live on.
110
89
  cleanup = nil
111
90
  @plock.synchronize do
@@ -113,25 +92,42 @@ module Sidekiq
113
92
  end
114
93
 
115
94
  if cleanup.size > 0
116
- jobs = cleanup.map {|p| p.job }.compact
95
+ jobs = cleanup.map { |p| p.job }.compact
117
96
 
118
- logger.warn { "Terminating #{cleanup.size} busy worker threads" }
119
- logger.warn { "Work still in progress #{jobs.inspect}" }
97
+ logger.warn { "Terminating #{cleanup.size} busy threads" }
98
+ logger.debug { "Jobs still in progress #{jobs.inspect}" }
120
99
 
121
100
  # Re-enqueue unfinished jobs
122
101
  # NOTE: You may notice that we may push a job back to redis before
123
- # the worker thread is terminated. This is ok because Sidekiq's
102
+ # the thread is terminated. This is ok because Sidekiq's
124
103
  # contract says that jobs are run AT LEAST once. Process termination
125
104
  # is delayed until we're certain the jobs are back in Redis because
126
105
  # it is worse to lose a job than to run it twice.
127
- strategy = (@options[:fetch] || Sidekiq::BasicFetch)
128
- strategy.bulk_requeue(jobs, @options)
106
+ capsule.fetcher.bulk_requeue(jobs)
129
107
  end
130
108
 
131
109
  cleanup.each do |processor|
132
110
  processor.kill
133
111
  end
112
+
113
+ # when this method returns, we immediately call `exit` which may not give
114
+ # the remaining threads time to run `ensure` blocks, etc. We pause here up
115
+ # to 3 seconds to give threads a minimal amount of time to run `ensure` blocks.
116
+ deadline = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC) + 3
117
+ wait_for(deadline) { @workers.empty? }
134
118
  end
135
119
 
120
+ # hack for quicker development / testing environment #2774
121
+ PAUSE_TIME = $stdout.tty? ? 0.1 : 0.5
122
+
123
+ # Wait for the orblock to be true or the deadline passed.
124
+ def wait_for(deadline, &condblock)
125
+ remaining = deadline - ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
126
+ while remaining > PAUSE_TIME
127
+ return if condblock.call
128
+ sleep PAUSE_TIME
129
+ remaining = deadline - ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
130
+ end
131
+ end
136
132
  end
137
133
  end