sidekiq 7.3.9 → 8.1.7

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 (155) hide show
  1. checksums.yaml +4 -4
  2. data/Changes.md +225 -0
  3. data/README.md +34 -14
  4. data/bin/kiq +17 -0
  5. data/bin/lint-herb +13 -0
  6. data/bin/sidekiqload +12 -10
  7. data/bin/webload +69 -0
  8. data/lib/active_job/queue_adapters/sidekiq_adapter.rb +107 -58
  9. data/lib/generators/sidekiq/job_generator.rb +15 -3
  10. data/lib/generators/sidekiq/templates/job.rb.erb +1 -1
  11. data/lib/sidekiq/api.rb +310 -83
  12. data/lib/sidekiq/capsule.rb +10 -7
  13. data/lib/sidekiq/cli.rb +31 -24
  14. data/lib/sidekiq/client.rb +36 -18
  15. data/lib/sidekiq/component.rb +45 -3
  16. data/lib/sidekiq/config.rb +35 -30
  17. data/lib/sidekiq/embedded.rb +2 -1
  18. data/lib/sidekiq/fetch.rb +1 -0
  19. data/lib/sidekiq/iterable_job.rb +1 -0
  20. data/lib/sidekiq/job/iterable.rb +46 -18
  21. data/lib/sidekiq/job.rb +18 -3
  22. data/lib/sidekiq/job_logger.rb +8 -6
  23. data/lib/sidekiq/job_retry.rb +45 -18
  24. data/lib/sidekiq/job_util.rb +5 -1
  25. data/lib/sidekiq/launcher.rb +22 -12
  26. data/lib/sidekiq/loader.rb +57 -0
  27. data/lib/sidekiq/logger.rb +25 -69
  28. data/lib/sidekiq/manager.rb +2 -2
  29. data/lib/sidekiq/metrics/query.rb +77 -46
  30. data/lib/sidekiq/metrics/shared.rb +8 -5
  31. data/lib/sidekiq/metrics/tracking.rb +12 -7
  32. data/lib/sidekiq/middleware/current_attributes.rb +11 -19
  33. data/lib/sidekiq/middleware/i18n.rb +2 -0
  34. data/lib/sidekiq/monitor.rb +4 -8
  35. data/lib/sidekiq/paginator.rb +18 -2
  36. data/lib/sidekiq/processor.rb +26 -18
  37. data/lib/sidekiq/profiler.rb +73 -0
  38. data/lib/sidekiq/rails.rb +46 -67
  39. data/lib/sidekiq/redis_client_adapter.rb +6 -3
  40. data/lib/sidekiq/redis_connection.rb +16 -5
  41. data/lib/sidekiq/ring_buffer.rb +1 -0
  42. data/lib/sidekiq/scheduled.rb +6 -7
  43. data/lib/sidekiq/test_api.rb +331 -0
  44. data/lib/sidekiq/testing/inline.rb +2 -30
  45. data/lib/sidekiq/testing.rb +2 -333
  46. data/lib/sidekiq/transaction_aware_client.rb +13 -5
  47. data/lib/sidekiq/tui/controls.rb +53 -0
  48. data/lib/sidekiq/tui/filtering.rb +53 -0
  49. data/lib/sidekiq/tui/tabs/base_tab.rb +230 -0
  50. data/lib/sidekiq/tui/tabs/busy.rb +104 -0
  51. data/lib/sidekiq/tui/tabs/dead.rb +14 -0
  52. data/lib/sidekiq/tui/tabs/home.rb +115 -0
  53. data/lib/sidekiq/tui/tabs/metrics.rb +114 -0
  54. data/lib/sidekiq/tui/tabs/queues.rb +83 -0
  55. data/lib/sidekiq/tui/tabs/retries.rb +14 -0
  56. data/lib/sidekiq/tui/tabs/scheduled.rb +14 -0
  57. data/lib/sidekiq/tui/tabs/set_tab.rb +91 -0
  58. data/lib/sidekiq/tui/tabs.rb +15 -0
  59. data/lib/sidekiq/tui.rb +390 -0
  60. data/lib/sidekiq/version.rb +2 -2
  61. data/lib/sidekiq/web/action.rb +146 -83
  62. data/lib/sidekiq/web/application.rb +372 -328
  63. data/lib/sidekiq/web/config.rb +117 -0
  64. data/lib/sidekiq/web/helpers.rb +102 -44
  65. data/lib/sidekiq/web/router.rb +61 -77
  66. data/lib/sidekiq/web.rb +65 -151
  67. data/lib/sidekiq.rb +13 -1
  68. data/sidekiq.gemspec +9 -8
  69. data/web/assets/images/logo.png +0 -0
  70. data/web/assets/images/status.png +0 -0
  71. data/web/assets/javascripts/application.js +43 -27
  72. data/web/assets/javascripts/base-charts.js +30 -16
  73. data/web/assets/javascripts/chartjs-adapter-date-fns.min.js +7 -0
  74. data/web/assets/javascripts/dashboard.js +1 -1
  75. data/web/assets/javascripts/metrics.js +16 -34
  76. data/web/assets/stylesheets/style.css +776 -0
  77. data/web/locales/ar.yml +3 -1
  78. data/web/locales/cs.yml +2 -0
  79. data/web/locales/da.yml +2 -0
  80. data/web/locales/de.yml +2 -0
  81. data/web/locales/el.yml +2 -0
  82. data/web/locales/en.yml +8 -0
  83. data/web/locales/es.yml +25 -2
  84. data/web/locales/fa.yml +3 -1
  85. data/web/locales/fr.yml +3 -1
  86. data/web/locales/gd.yml +13 -1
  87. data/web/locales/he.yml +3 -1
  88. data/web/locales/hi.yml +36 -0
  89. data/web/locales/it.yml +9 -0
  90. data/web/locales/ja.yml +2 -0
  91. data/web/locales/ko.yml +2 -0
  92. data/web/locales/lt.yml +2 -0
  93. data/web/locales/nb.yml +2 -0
  94. data/web/locales/nl.yml +2 -0
  95. data/web/locales/pl.yml +2 -0
  96. data/web/locales/{pt-br.yml → pt-BR.yml} +3 -1
  97. data/web/locales/pt.yml +2 -0
  98. data/web/locales/ru.yml +2 -0
  99. data/web/locales/sv.yml +2 -0
  100. data/web/locales/ta.yml +83 -47
  101. data/web/locales/tr.yml +2 -0
  102. data/web/locales/uk.yml +7 -5
  103. data/web/locales/ur.yml +3 -1
  104. data/web/locales/vi.yml +2 -0
  105. data/web/locales/{zh-cn.yml → zh-CN.yml} +86 -73
  106. data/web/locales/{zh-tw.yml → zh-TW.yml} +3 -1
  107. data/web/views/_footer.html.erb +32 -0
  108. data/web/views/_job_info.html.erb +115 -0
  109. data/web/views/_metrics_period_select.html.erb +15 -0
  110. data/web/views/_nav.html.erb +45 -0
  111. data/web/views/_paging.html.erb +26 -0
  112. data/web/views/_poll_link.html.erb +4 -0
  113. data/web/views/{_summary.erb → _summary.html.erb} +16 -16
  114. data/web/views/busy.html.erb +155 -0
  115. data/web/views/dashboard.html.erb +104 -0
  116. data/web/views/dead.html.erb +38 -0
  117. data/web/views/filtering.html.erb +6 -0
  118. data/web/views/layout.html.erb +26 -0
  119. data/web/views/metrics.html.erb +85 -0
  120. data/web/views/metrics_for_job.html.erb +58 -0
  121. data/web/views/morgue.html.erb +69 -0
  122. data/web/views/profiles.html.erb +43 -0
  123. data/web/views/queue.html.erb +57 -0
  124. data/web/views/queues.html.erb +46 -0
  125. data/web/views/retries.html.erb +77 -0
  126. data/web/views/retry.html.erb +39 -0
  127. data/web/views/scheduled.html.erb +64 -0
  128. data/web/views/{scheduled_job_info.erb → scheduled_job_info.html.erb} +3 -3
  129. metadata +65 -47
  130. data/lib/sidekiq/web/csrf_protection.rb +0 -183
  131. data/web/assets/stylesheets/application-dark.css +0 -147
  132. data/web/assets/stylesheets/application-rtl.css +0 -163
  133. data/web/assets/stylesheets/application.css +0 -759
  134. data/web/assets/stylesheets/bootstrap-rtl.min.css +0 -9
  135. data/web/assets/stylesheets/bootstrap.css +0 -5
  136. data/web/views/_footer.erb +0 -34
  137. data/web/views/_job_info.erb +0 -105
  138. data/web/views/_metrics_period_select.erb +0 -12
  139. data/web/views/_nav.erb +0 -52
  140. data/web/views/_paging.erb +0 -25
  141. data/web/views/_poll_link.erb +0 -4
  142. data/web/views/_status.erb +0 -4
  143. data/web/views/busy.erb +0 -148
  144. data/web/views/dashboard.erb +0 -108
  145. data/web/views/dead.erb +0 -34
  146. data/web/views/filtering.erb +0 -6
  147. data/web/views/layout.erb +0 -42
  148. data/web/views/metrics.erb +0 -90
  149. data/web/views/metrics_for_job.erb +0 -59
  150. data/web/views/morgue.erb +0 -74
  151. data/web/views/queue.erb +0 -55
  152. data/web/views/queues.erb +0 -44
  153. data/web/views/retries.erb +0 -79
  154. data/web/views/retry.erb +0 -34
  155. data/web/views/scheduled.erb +0 -56
data/lib/sidekiq/job.rb CHANGED
@@ -196,7 +196,7 @@ module Sidekiq
196
196
 
197
197
  def set(options)
198
198
  hash = options.transform_keys(&:to_s)
199
- interval = hash.delete("wait_until") || @opts.delete("wait")
199
+ interval = hash.delete("wait_until") || hash.delete("wait")
200
200
  @opts.merge!(hash)
201
201
  at(interval) if interval
202
202
  self
@@ -226,6 +226,8 @@ module Sidekiq
226
226
  end
227
227
  return nil unless result
228
228
 
229
+ verify_json(item)
230
+
229
231
  # round-trip the payload via JSON
230
232
  msg = Sidekiq.load_json(Sidekiq.dump_json(item))
231
233
 
@@ -248,9 +250,9 @@ module Sidekiq
248
250
  end
249
251
  alias_method :perform_sync, :perform_inline
250
252
 
251
- def perform_bulk(args, batch_size: 1_000)
253
+ def perform_bulk(args, **options)
252
254
  client = @klass.build_client
253
- client.push_bulk(@opts.merge("class" => @klass, "args" => args, :batch_size => batch_size))
255
+ client.push_bulk(@opts.merge({"class" => @klass, "args" => args}, options))
254
256
  end
255
257
 
256
258
  # +interval+ must be a timestamp, numeric or something that acts
@@ -311,6 +313,11 @@ module Sidekiq
311
313
  #
312
314
  # +items+ must be an Array of Arrays.
313
315
  #
316
+ # The +:at+ option schedules the jobs for future execution. It accepts
317
+ # either a single Numeric timestamp (or seconds-from-now) applied to every
318
+ # job, or an Array of Numeric values with the same size as +items+ to
319
+ # schedule each job at its corresponding time.
320
+ #
314
321
  # For finer-grained control, use `Sidekiq::Client.push_bulk` directly.
315
322
  #
316
323
  # Example (3 Redis round trips):
@@ -323,6 +330,14 @@ module Sidekiq
323
330
  #
324
331
  # SomeJob.perform_bulk([[1], [2], [3]])
325
332
  #
333
+ # Scheduling every job 60 seconds from now (single Numeric +:at+):
334
+ #
335
+ # SomeJob.perform_bulk([[1], [2], [3]], at: 60)
336
+ #
337
+ # Scheduling each job at its own time (Array +:at+):
338
+ #
339
+ # SomeJob.perform_bulk([[1], [2]], at: [Time.now.to_f + 30, Time.now.to_f + 60])
340
+ #
326
341
  def perform_bulk(*args, **kwargs)
327
342
  Setter.new(self, {}).perform_bulk(*args, **kwargs)
328
343
  end
@@ -26,16 +26,18 @@ module Sidekiq
26
26
  # If we're using a wrapper class, like ActiveJob, use the "wrapped"
27
27
  # attribute to expose the underlying thing.
28
28
  h = {
29
- class: job_hash["display_class"] || job_hash["wrapped"] || job_hash["class"],
30
- jid: job_hash["jid"]
29
+ jid: job_hash["jid"],
30
+ class: job_hash["wrapped"] || job_hash["class"]
31
31
  }
32
- h[:bid] = job_hash["bid"] if job_hash.has_key?("bid")
33
- h[:tags] = job_hash["tags"] if job_hash.has_key?("tags")
32
+
33
+ @config[:logged_job_attributes].each do |attr|
34
+ h[attr.to_sym] = job_hash[attr] if job_hash.has_key?(attr)
35
+ end
34
36
 
35
37
  Thread.current[:sidekiq_context] = h
36
38
  level = job_hash["log_level"]
37
- if level && @logger.respond_to?(:log_at)
38
- @logger.log_at(level, &block)
39
+ if level
40
+ @logger.with_level(level, &block)
39
41
  else
40
42
  yield
41
43
  end
@@ -72,8 +72,8 @@ module Sidekiq
72
72
 
73
73
  DEFAULT_MAX_RETRY_ATTEMPTS = 25
74
74
 
75
- def initialize(capsule)
76
- @config = @capsule = capsule
75
+ def initialize(cfg)
76
+ @config = cfg
77
77
  @max_retries = Sidekiq.default_configuration[:max_retries] || DEFAULT_MAX_RETRY_ATTEMPTS
78
78
  @backtrace_cleaner = Sidekiq.default_configuration[:backtrace_cleaner]
79
79
  end
@@ -96,7 +96,7 @@ module Sidekiq
96
96
  if msg["retry"]
97
97
  process_retry(nil, msg, queue, e)
98
98
  else
99
- @capsule.config.death_handlers.each do |handler|
99
+ @config[:death_handlers].each do |handler|
100
100
  handler.call(msg, e)
101
101
  rescue => handler_ex
102
102
  handle_exception(handler_ex, {context: "Error calling death handler", job: msg})
@@ -139,6 +139,10 @@ module Sidekiq
139
139
 
140
140
  private
141
141
 
142
+ def now_ms
143
+ ::Process.clock_gettime(::Process::CLOCK_REALTIME, :millisecond)
144
+ end
145
+
142
146
  # Note that +jobinst+ can be nil here if an error is raised before we can
143
147
  # instantiate the job instance. All access must be guarded and
144
148
  # best effort.
@@ -149,17 +153,17 @@ module Sidekiq
149
153
 
150
154
  m = exception_message(exception)
151
155
  if m.respond_to?(:scrub!)
152
- m.force_encoding("utf-8")
156
+ m.force_encoding(Encoding::UTF_8)
153
157
  m.scrub!
154
158
  end
155
159
 
156
160
  msg["error_message"] = m
157
161
  msg["error_class"] = exception.class.name
158
162
  count = if msg["retry_count"]
159
- msg["retried_at"] = Time.now.to_f
163
+ msg["retried_at"] = now_ms
160
164
  msg["retry_count"] += 1
161
165
  else
162
- msg["failed_at"] = Time.now.to_f
166
+ msg["failed_at"] = now_ms
163
167
  msg["retry_count"] = 0
164
168
  end
165
169
 
@@ -174,22 +178,28 @@ module Sidekiq
174
178
  msg["error_backtrace"] = compress_backtrace(lines)
175
179
  end
176
180
 
177
- return retries_exhausted(jobinst, msg, exception) if count >= max_retry_attempts
178
-
181
+ # retry_for and retry are mutually exclusive - if retry_for is set,
182
+ # we exclusively use duration-based retry logic and ignore count-based logic
179
183
  rf = msg["retry_for"]
180
- return retries_exhausted(jobinst, msg, exception) if rf && ((msg["failed_at"] + rf) < Time.now.to_f)
184
+ if rf
185
+ return retries_exhausted(jobinst, msg, exception) if (time_for(msg["failed_at"]) + rf) < Time.now
186
+ elsif count >= max_retry_attempts
187
+ return retries_exhausted(jobinst, msg, exception)
188
+ end
181
189
 
182
190
  strategy, delay = delay_for(jobinst, count, exception, msg)
183
191
  case strategy
184
192
  when :discard
185
- return # poof!
193
+ msg["discarded_at"] = now_ms
194
+
195
+ return run_death_handlers(msg, exception)
186
196
  when :kill
187
197
  return retries_exhausted(jobinst, msg, exception)
188
198
  end
189
199
 
190
200
  # Logging here can break retries if the logging device raises ENOSPC #3979
191
201
  # logger.debug { "Failure! Retry #{count} in #{delay} seconds" }
192
- jitter = rand(10) * (count + 1)
202
+ jitter = rand(10 * (count + 1))
193
203
  retry_at = Time.now.to_f + delay + jitter
194
204
  payload = Sidekiq.dump_json(msg)
195
205
  redis do |conn|
@@ -197,6 +207,14 @@ module Sidekiq
197
207
  end
198
208
  end
199
209
 
210
+ def time_for(item)
211
+ if item.is_a?(Float)
212
+ Time.at(item)
213
+ else
214
+ Time.at(item / 1000, item % 1000, :millisecond)
215
+ end
216
+ end
217
+
200
218
  # returns (strategy, seconds)
201
219
  def delay_for(jobinst, count, exception, msg)
202
220
  rv = begin
@@ -243,13 +261,22 @@ module Sidekiq
243
261
  handle_exception(e, {context: "Error calling retries_exhausted", job: msg})
244
262
  end
245
263
 
246
- return if rv == :discard # poof!
247
- send_to_morgue(msg) unless msg["dead"] == false
264
+ discarded = msg["dead"] == false || rv == :discard
265
+
266
+ if discarded
267
+ msg["discarded_at"] = now_ms
268
+ else
269
+ send_to_morgue(msg)
270
+ end
271
+
272
+ run_death_handlers(msg, exception)
273
+ end
248
274
 
249
- @capsule.config.death_handlers.each do |handler|
250
- handler.call(msg, exception)
275
+ def run_death_handlers(job, exception)
276
+ @config[:death_handlers].each do |handler|
277
+ handler.call(job, exception)
251
278
  rescue => e
252
- handle_exception(e, {context: "Error calling death handler", job: msg})
279
+ handle_exception(e, {context: "Error calling death handler", job: job})
253
280
  end
254
281
  end
255
282
 
@@ -261,8 +288,8 @@ module Sidekiq
261
288
  redis do |conn|
262
289
  conn.multi do |xa|
263
290
  xa.zadd("dead", now.to_s, payload)
264
- xa.zremrangebyscore("dead", "-inf", now - @capsule.config[:dead_timeout_in_seconds])
265
- xa.zremrangebyrank("dead", 0, - @capsule.config[:dead_max_jobs])
291
+ xa.zremrangebyscore("dead", "-inf", now - @config[:dead_timeout_in_seconds])
292
+ xa.zremrangebyrank("dead", 0, - @config[:dead_max_jobs])
266
293
  end
267
294
  end
268
295
  end
@@ -58,10 +58,14 @@ module Sidekiq
58
58
  item["class"] = item["class"].to_s
59
59
  item["queue"] = item["queue"].to_s
60
60
  item["retry_for"] = item["retry_for"].to_i if item["retry_for"]
61
- item["created_at"] ||= Time.now.to_f
61
+ item["created_at"] ||= now_in_millis
62
62
  item
63
63
  end
64
64
 
65
+ def now_in_millis
66
+ ::Process.clock_gettime(::Process::CLOCK_REALTIME, :millisecond)
67
+ end
68
+
65
69
  def normalized_hash(item_class)
66
70
  if item_class.is_a?(Class)
67
71
  raise(ArgumentError, "Message must include a Sidekiq::Job class, not class name: #{item_class.ancestors.inspect}") unless item_class.respond_to?(:get_sidekiq_options)
@@ -68,6 +68,7 @@ module Sidekiq
68
68
  stoppers.each(&:join)
69
69
 
70
70
  clear_heartbeat
71
+ fire_event(:exit, reverse: true)
71
72
  end
72
73
 
73
74
  def stopping?
@@ -81,7 +82,7 @@ module Sidekiq
81
82
  ❤
82
83
  end
83
84
 
84
- private unless $TESTING
85
+ private
85
86
 
86
87
  BEAT_PAUSE = 10
87
88
 
@@ -141,6 +142,12 @@ module Sidekiq
141
142
  key = identity
142
143
  fails = procd = 0
143
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
+
144
151
  begin
145
152
  flush_stats
146
153
 
@@ -169,6 +176,7 @@ module Sidekiq
169
176
  transaction.sadd("processes", [key])
170
177
  transaction.exists(key)
171
178
  transaction.hset(key, "info", to_json,
179
+ "concurrency", @config.total_concurrency,
172
180
  "busy", curstate.size,
173
181
  "beat", Time.now.to_f,
174
182
  "rtt_us", rtt,
@@ -213,11 +221,11 @@ module Sidekiq
213
221
  # Log a warning if it's a disaster.
214
222
  if RTT_READINGS.all? { |x| x > RTT_WARNING_LEVEL }
215
223
  logger.warn <<~EOM
216
- Your Redis network connection is performing extremely poorly.
224
+ Your Redis network connection appears to be performing poorly.
217
225
  Last RTT readings were #{RTT_READINGS.buffer.inspect}, ideally these should be < 1000.
218
- Ensure Redis is running in the same AZ or datacenter as Sidekiq.
219
- If these values are close to 100,000, that means your Sidekiq process may be
220
- CPU-saturated; reduce your concurrency and/or see https://github.com/sidekiq/sidekiq/discussions/5039
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
221
229
  EOM
222
230
  RTT_READINGS.reset
223
231
  end
@@ -227,7 +235,7 @@ module Sidekiq
227
235
  MEMORY_GRABBER = case RUBY_PLATFORM
228
236
  when /linux/
229
237
  ->(pid) {
230
- IO.readlines("/proc/#{$$}/status").each do |line|
238
+ IO.readlines("/proc/#{pid}/status").each do |line|
231
239
  next unless line.start_with?("VmRSS:")
232
240
  break line.split[1].to_i
233
241
  end
@@ -250,9 +258,15 @@ module Sidekiq
250
258
  "started_at" => Time.now.to_f,
251
259
  "pid" => ::Process.pid,
252
260
  "tag" => @config[:tag] || "",
253
- "concurrency" => @config.total_concurrency,
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
254
267
  "queues" => @config.capsules.values.flat_map { |cap| cap.queues }.uniq,
255
- "weights" => to_weights,
268
+ "weights" => @config.capsules.values.map(&:weights),
269
+ #####
256
270
  "labels" => @config[:labels].to_a,
257
271
  "identity" => identity,
258
272
  "version" => Sidekiq::VERSION,
@@ -260,10 +274,6 @@ module Sidekiq
260
274
  }
261
275
  end
262
276
 
263
- def to_weights
264
- @config.capsules.values.map(&:weights)
265
- end
266
-
267
277
  def to_json
268
278
  # this data changes infrequently so dump it to a string
269
279
  # now so we don't need to dump it every heartbeat.
@@ -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
@@ -22,92 +22,48 @@ module Sidekiq
22
22
  end
23
23
  end
24
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
25
  class Logger < ::Logger
76
- include LoggingUtils
77
-
78
26
  module Formatters
79
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
+
80
36
  def tid
81
37
  Thread.current["sidekiq_tid"] ||= (Thread.current.object_id ^ ::Process.pid).to_s(36)
82
38
  end
83
39
 
84
- def ctx
85
- Sidekiq::Context.current
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(" ")}"
86
49
  end
50
+ end
87
51
 
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
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"
99
55
  end
100
56
  end
101
57
 
102
- class Pretty < Base
58
+ class Plain < Base
103
59
  def call(severity, time, program_name, message)
104
- "#{time.utc.iso8601(3)} pid=#{::Process.pid} tid=#{tid}#{format_context} #{severity}: #{message}\n"
60
+ "#{severity} #{time.utc.iso8601(3)} pid=#{::Process.pid} tid=#{tid}#{format_context}: #{message}\n"
105
61
  end
106
62
  end
107
63
 
108
64
  class WithoutTimestamp < Pretty
109
65
  def call(severity, time, program_name, message)
110
- "pid=#{::Process.pid} tid=#{tid}#{format_context} #{severity}: #{message}\n"
66
+ "#{COLORS[severity]} pid=#{::Process.pid} tid=#{tid}#{format_context}: #{message}\n"
111
67
  end
112
68
  end
113
69
 
@@ -120,7 +76,7 @@ module Sidekiq
120
76
  lvl: severity,
121
77
  msg: message
122
78
  }
123
- c = ctx
79
+ c = Sidekiq::Context.current
124
80
  hash["ctx"] = c unless c.empty?
125
81
 
126
82
  Sidekiq.dump_json(hash) << "\n"
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "sidekiq/processor"
4
- require "set"
5
4
 
6
5
  module Sidekiq
7
6
  ##
@@ -70,12 +69,13 @@ module Sidekiq
70
69
  def processor_result(processor, reason = nil)
71
70
  @plock.synchronize do
72
71
  @workers.delete(processor)
73
- unless @done
72
+ if !@done && @count > @workers.size
74
73
  p = Processor.new(@config, &method(:processor_result))
75
74
  @workers << p
76
75
  p.start
77
76
  end
78
77
  end
78
+ nil
79
79
  end
80
80
 
81
81
  def stopped?