sidekiq 5.2.7 → 7.0.2

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.

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