sidekiq 7.3.0 → 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 (115) hide show
  1. checksums.yaml +4 -4
  2. data/Changes.md +158 -0
  3. data/README.md +16 -13
  4. data/bin/sidekiqload +31 -22
  5. data/bin/webload +69 -0
  6. data/lib/active_job/queue_adapters/sidekiq_adapter.rb +120 -0
  7. data/lib/generators/sidekiq/job_generator.rb +2 -0
  8. data/lib/sidekiq/api.rb +184 -71
  9. data/lib/sidekiq/capsule.rb +11 -9
  10. data/lib/sidekiq/cli.rb +16 -20
  11. data/lib/sidekiq/client.rb +28 -11
  12. data/lib/sidekiq/component.rb +62 -2
  13. data/lib/sidekiq/config.rb +42 -18
  14. data/lib/sidekiq/deploy.rb +2 -0
  15. data/lib/sidekiq/embedded.rb +4 -1
  16. data/lib/sidekiq/iterable_job.rb +3 -0
  17. data/lib/sidekiq/job/interrupt_handler.rb +2 -0
  18. data/lib/sidekiq/job/iterable/active_record_enumerator.rb +3 -3
  19. data/lib/sidekiq/job/iterable.rb +82 -7
  20. data/lib/sidekiq/job_logger.rb +15 -27
  21. data/lib/sidekiq/job_retry.rb +17 -5
  22. data/lib/sidekiq/job_util.rb +7 -1
  23. data/lib/sidekiq/launcher.rb +3 -2
  24. data/lib/sidekiq/logger.rb +19 -70
  25. data/lib/sidekiq/manager.rb +0 -1
  26. data/lib/sidekiq/metrics/query.rb +73 -45
  27. data/lib/sidekiq/metrics/shared.rb +23 -9
  28. data/lib/sidekiq/metrics/tracking.rb +22 -12
  29. data/lib/sidekiq/middleware/current_attributes.rb +12 -4
  30. data/lib/sidekiq/middleware/modules.rb +2 -0
  31. data/lib/sidekiq/monitor.rb +2 -1
  32. data/lib/sidekiq/paginator.rb +14 -1
  33. data/lib/sidekiq/processor.rb +26 -19
  34. data/lib/sidekiq/profiler.rb +72 -0
  35. data/lib/sidekiq/rails.rb +44 -55
  36. data/lib/sidekiq/redis_client_adapter.rb +0 -1
  37. data/lib/sidekiq/redis_connection.rb +22 -4
  38. data/lib/sidekiq/ring_buffer.rb +2 -0
  39. data/lib/sidekiq/systemd.rb +2 -0
  40. data/lib/sidekiq/testing.rb +7 -7
  41. data/lib/sidekiq/version.rb +6 -2
  42. data/lib/sidekiq/web/action.rb +124 -69
  43. data/lib/sidekiq/web/application.rb +355 -377
  44. data/lib/sidekiq/web/config.rb +120 -0
  45. data/lib/sidekiq/web/helpers.rb +64 -33
  46. data/lib/sidekiq/web/router.rb +61 -74
  47. data/lib/sidekiq/web.rb +52 -150
  48. data/lib/sidekiq.rb +5 -4
  49. data/sidekiq.gemspec +6 -6
  50. data/web/assets/javascripts/application.js +6 -13
  51. data/web/assets/javascripts/base-charts.js +30 -16
  52. data/web/assets/javascripts/chartjs-adapter-date-fns.min.js +7 -0
  53. data/web/assets/javascripts/dashboard-charts.js +2 -0
  54. data/web/assets/javascripts/dashboard.js +7 -1
  55. data/web/assets/javascripts/metrics.js +16 -34
  56. data/web/assets/stylesheets/style.css +766 -0
  57. data/web/locales/ar.yml +1 -0
  58. data/web/locales/cs.yml +1 -0
  59. data/web/locales/da.yml +1 -0
  60. data/web/locales/de.yml +1 -0
  61. data/web/locales/el.yml +1 -0
  62. data/web/locales/en.yml +9 -1
  63. data/web/locales/es.yml +24 -2
  64. data/web/locales/fa.yml +1 -0
  65. data/web/locales/fr.yml +1 -1
  66. data/web/locales/gd.yml +1 -1
  67. data/web/locales/he.yml +1 -0
  68. data/web/locales/hi.yml +1 -0
  69. data/web/locales/it.yml +40 -1
  70. data/web/locales/ja.yml +1 -1
  71. data/web/locales/ko.yml +1 -0
  72. data/web/locales/lt.yml +1 -0
  73. data/web/locales/nb.yml +1 -0
  74. data/web/locales/nl.yml +1 -0
  75. data/web/locales/pl.yml +1 -0
  76. data/web/locales/{pt-br.yml → pt-BR.yml} +3 -3
  77. data/web/locales/pt.yml +1 -0
  78. data/web/locales/ru.yml +1 -0
  79. data/web/locales/sv.yml +1 -0
  80. data/web/locales/ta.yml +1 -0
  81. data/web/locales/tr.yml +2 -2
  82. data/web/locales/uk.yml +25 -1
  83. data/web/locales/ur.yml +1 -0
  84. data/web/locales/vi.yml +1 -0
  85. data/web/locales/{zh-cn.yml → zh-CN.yml} +85 -74
  86. data/web/locales/{zh-tw.yml → zh-TW.yml} +2 -2
  87. data/web/views/_footer.erb +31 -34
  88. data/web/views/_job_info.erb +91 -89
  89. data/web/views/_metrics_period_select.erb +13 -10
  90. data/web/views/_nav.erb +14 -21
  91. data/web/views/_paging.erb +23 -21
  92. data/web/views/_poll_link.erb +2 -2
  93. data/web/views/_summary.erb +16 -16
  94. data/web/views/busy.erb +124 -122
  95. data/web/views/dashboard.erb +63 -64
  96. data/web/views/dead.erb +31 -27
  97. data/web/views/filtering.erb +3 -4
  98. data/web/views/layout.erb +13 -29
  99. data/web/views/metrics.erb +75 -82
  100. data/web/views/metrics_for_job.erb +45 -46
  101. data/web/views/morgue.erb +61 -70
  102. data/web/views/profiles.erb +43 -0
  103. data/web/views/queue.erb +54 -52
  104. data/web/views/queues.erb +43 -41
  105. data/web/views/retries.erb +66 -75
  106. data/web/views/retry.erb +32 -27
  107. data/web/views/scheduled.erb +59 -55
  108. data/web/views/scheduled_job_info.erb +1 -1
  109. metadata +27 -29
  110. data/web/assets/stylesheets/application-dark.css +0 -147
  111. data/web/assets/stylesheets/application-rtl.css +0 -163
  112. data/web/assets/stylesheets/application.css +0 -758
  113. data/web/assets/stylesheets/bootstrap-rtl.min.css +0 -9
  114. data/web/assets/stylesheets/bootstrap.css +0 -5
  115. data/web/views/_status.erb +0 -4
@@ -1,6 +1,6 @@
1
- require "forwardable"
1
+ # frozen_string_literal: true
2
2
 
3
- require "set"
3
+ require "forwardable"
4
4
  require "sidekiq/redis_connection"
5
5
 
6
6
  module Sidekiq
@@ -27,6 +27,7 @@ module Sidekiq
27
27
  startup: [],
28
28
  quiet: [],
29
29
  shutdown: [],
30
+ exit: [],
30
31
  # triggers when we fire the first heartbeat on startup OR repairing a network partition
31
32
  heartbeat: [],
32
33
  # triggers on EVERY heartbeat call, every 10 seconds
@@ -39,12 +40,22 @@ module Sidekiq
39
40
  }
40
41
 
41
42
  ERROR_HANDLER = ->(ex, ctx, cfg = Sidekiq.default_configuration) {
42
- l = cfg.logger
43
- l.warn(Sidekiq.dump_json(ctx)) unless ctx.empty?
44
- l.warn("#{ex.class.name}: #{ex.message}")
45
- unless ex.backtrace.nil?
46
- backtrace = cfg[:backtrace_cleaner].call(ex.backtrace)
47
- l.warn(backtrace.join("\n"))
43
+ Sidekiq::Context.with(ctx) do
44
+ dev = cfg[:environment] == "development"
45
+ fancy = dev && $stdout.tty? # 🎩
46
+ # Weird logic here but we want to show the backtrace in local
47
+ # development or if verbose logging is enabled.
48
+ #
49
+ # `full_message` contains the error class, message and backtrace
50
+ # `detailed_message` contains the error class and message
51
+ #
52
+ # Absolutely terrible API names. Not useful at all to have two
53
+ # methods with similar but obscure names.
54
+ if dev || cfg.logger.debug?
55
+ cfg.logger.info { ex.full_message(highlight: fancy) }
56
+ else
57
+ cfg.logger.info { ex.detailed_message(highlight: fancy) }
58
+ end
48
59
  end
49
60
  }
50
61
 
@@ -58,6 +69,13 @@ module Sidekiq
58
69
 
59
70
  def_delegators :@options, :[], :[]=, :fetch, :key?, :has_key?, :merge!, :dig
60
71
  attr_reader :capsules
72
+ attr_accessor :thread_priority
73
+
74
+ def inspect
75
+ "#<#{self.class.name} @options=#{
76
+ @options.except(:lifecycle_events, :reloader, :death_handlers, :error_handlers).inspect
77
+ }>"
78
+ end
61
79
 
62
80
  def to_json(*)
63
81
  Sidekiq.dump_json(@options)
@@ -183,7 +201,13 @@ module Sidekiq
183
201
 
184
202
  # register global singletons which can be accessed elsewhere
185
203
  def register(name, instance)
186
- @directory[name] = instance
204
+ # logger.debug("register[#{name}] = #{instance}")
205
+ # Sidekiq Enterprise lazy registers a few services so we
206
+ # can't lock down this hash completely.
207
+ hash = @directory.dup
208
+ hash[name] = instance
209
+ @directory = hash.freeze
210
+ instance
187
211
  end
188
212
 
189
213
  # find a singleton
@@ -191,10 +215,16 @@ module Sidekiq
191
215
  # JNDI is just a fancy name for a hash lookup
192
216
  @directory.fetch(name) do |key|
193
217
  return nil unless default_class
194
- @directory[key] = default_class.new(self)
218
+ register(key, default_class.new(self))
195
219
  end
196
220
  end
197
221
 
222
+ def freeze!
223
+ @directory.freeze
224
+ @options.freeze
225
+ true
226
+ end
227
+
198
228
  ##
199
229
  # Death handlers are called when all retries for a job have been exhausted and
200
230
  # the job dies. It's the notification to your application
@@ -229,7 +259,7 @@ module Sidekiq
229
259
  end
230
260
 
231
261
  # Register a block to run at a point in the Sidekiq lifecycle.
232
- # :startup, :quiet or :shutdown are valid events.
262
+ # :startup, :quiet, :shutdown, or :exit are valid events.
233
263
  #
234
264
  # Sidekiq.configure_server do |config|
235
265
  # config.on(:shutdown) do
@@ -273,13 +303,7 @@ module Sidekiq
273
303
  p ["!!!!!", ex]
274
304
  end
275
305
  @options[:error_handlers].each do |handler|
276
- if parameter_size(handler) == 2
277
- # TODO Remove in 8.0
278
- logger.info { "DEPRECATION: Sidekiq exception handlers now take three arguments, see #{handler}" }
279
- handler.call(ex, {_config: self}.merge(ctx))
280
- else
281
- handler.call(ex, ctx, self)
282
- end
306
+ handler.call(ex, ctx, self)
283
307
  rescue Exception => e
284
308
  l = logger
285
309
  l.error "!!! ERROR HANDLER THREW AN ERROR !!!"
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "sidekiq/redis_connection"
2
4
  require "time"
3
5
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "sidekiq/component"
2
4
  require "sidekiq/launcher"
3
5
  require "sidekiq/metrics/tracking"
@@ -32,6 +34,7 @@ module Sidekiq
32
34
  private
33
35
 
34
36
  def housekeeping
37
+ @config[:tag] ||= default_tag
35
38
  logger.info "Running in #{RUBY_DESCRIPTION}"
36
39
  logger.info Sidekiq::LICENSE
37
40
  logger.info "Upgrade to Sidekiq Pro for more features and support: https://sidekiq.org" unless defined?(::Sidekiq::Pro)
@@ -40,7 +43,7 @@ module Sidekiq
40
43
  # fire startup and start multithreading.
41
44
  info = config.redis_info
42
45
  ver = Gem::Version.new(info["redis_version"])
43
- raise "You are connecting to Redis #{ver}, Sidekiq requires Redis 6.2.0 or greater" if ver < Gem::Version.new("6.2.0")
46
+ raise "You are connected to Redis #{ver}, Sidekiq requires Redis 7.0.0 or greater" if ver < Gem::Version.new("7.0.0")
44
47
 
45
48
  maxmemory_policy = info["maxmemory_policy"]
46
49
  if maxmemory_policy != "noeviction"
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "sidekiq/job/iterable"
2
4
 
3
5
  # Iterable jobs are ones which provide a sequence to process using
@@ -44,6 +46,7 @@ module Sidekiq
44
46
  # def on_start
45
47
  # def on_resume
46
48
  # def on_stop
49
+ # def on_cancel
47
50
  # def on_complete
48
51
  # def around_iteration
49
52
  #
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Sidekiq
2
4
  module Job
3
5
  class InterruptHandler
@@ -22,7 +22,7 @@ module Sidekiq
22
22
  def batches
23
23
  Enumerator.new(-> { @relation.count }) do |yielder|
24
24
  @relation.find_in_batches(**@options, start: @cursor) do |batch|
25
- yielder.yield(batch, batch.last.id)
25
+ yielder.yield(batch, batch.first.id)
26
26
  end
27
27
  end
28
28
  end
@@ -35,8 +35,8 @@ module Sidekiq
35
35
  options[:of] ||= options.delete(:batch_size)
36
36
 
37
37
  @relation.in_batches(**options, start: @cursor) do |relation|
38
- last_record = relation.last
39
- yielder.yield(relation, last_record.id)
38
+ first_record = relation.first
39
+ yielder.yield(relation, first_record.id)
40
40
  end
41
41
  end
42
42
  end
@@ -30,6 +30,42 @@ module Sidekiq
30
30
  @_cursor = nil
31
31
  @_start_time = nil
32
32
  @_runtime = 0
33
+ @_args = nil
34
+ @_cancelled = nil
35
+ end
36
+
37
+ def arguments
38
+ @_args
39
+ end
40
+
41
+ # Three days is the longest period you generally need to wait for a retry to
42
+ # execute when using the default retry scheme. We don't want to "forget" the job
43
+ # is cancelled before it has a chance to execute and cancel itself.
44
+ CANCELLATION_PERIOD = (3 * 86_400).to_s
45
+
46
+ # Set a flag in Redis to mark this job as cancelled.
47
+ # Cancellation is asynchronous and is checked at the start of iteration
48
+ # and every 5 seconds thereafter as part of the recurring state flush.
49
+ def cancel!
50
+ return @_cancelled if cancelled?
51
+
52
+ key = "it-#{jid}"
53
+ _, result, _ = Sidekiq.redis do |c|
54
+ c.pipelined do |p|
55
+ p.hsetnx(key, "cancelled", Time.now.to_i)
56
+ p.hget(key, "cancelled")
57
+ p.expire(key, Sidekiq::Job::Iterable::STATE_TTL, "nx")
58
+ end
59
+ end
60
+ @_cancelled = result.to_i
61
+ end
62
+
63
+ def cancelled?
64
+ @_cancelled
65
+ end
66
+
67
+ def cursor
68
+ @_cursor.freeze
33
69
  end
34
70
 
35
71
  # A hook to override that will be called when the job starts iterating.
@@ -59,6 +95,11 @@ module Sidekiq
59
95
  def on_stop
60
96
  end
61
97
 
98
+ # A hook to override that will be called when the job is cancelled.
99
+ #
100
+ def on_cancel
101
+ end
102
+
62
103
  # A hook to override that will be called when the job finished iterating.
63
104
  #
64
105
  def on_complete
@@ -91,13 +132,14 @@ module Sidekiq
91
132
  end
92
133
 
93
134
  # @api private
94
- def perform(*arguments)
135
+ def perform(*args)
136
+ @_args = args.dup.freeze
95
137
  fetch_previous_iteration_state
96
138
 
97
139
  @_executions += 1
98
140
  @_start_time = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
99
141
 
100
- enumerator = build_enumerator(*arguments, cursor: @_cursor)
142
+ enumerator = build_enumerator(*args, cursor: @_cursor)
101
143
  unless enumerator
102
144
  logger.info("'#build_enumerator' returned nil, skipping the job.")
103
145
  return
@@ -112,7 +154,7 @@ module Sidekiq
112
154
  end
113
155
 
114
156
  completed = catch(:abort) do
115
- iterate_with_enumerator(enumerator, arguments)
157
+ iterate_with_enumerator(enumerator, args)
116
158
  end
117
159
 
118
160
  on_stop
@@ -128,6 +170,10 @@ module Sidekiq
128
170
 
129
171
  private
130
172
 
173
+ def is_cancelled?
174
+ @_cancelled = Sidekiq.redis { |c| c.hget("it-#{jid}", "cancelled") }
175
+ end
176
+
131
177
  def fetch_previous_iteration_state
132
178
  state = Sidekiq.redis { |conn| conn.hgetall(iteration_key) }
133
179
 
@@ -144,6 +190,13 @@ module Sidekiq
144
190
  STATE_TTL = 30 * 24 * 60 * 60 # one month
145
191
 
146
192
  def iterate_with_enumerator(enumerator, arguments)
193
+ if is_cancelled?
194
+ on_cancel
195
+ logger.info { "Job cancelled" }
196
+ return true
197
+ end
198
+
199
+ time_limit = Sidekiq.default_configuration[:timeout]
147
200
  found_record = false
148
201
  state_flushed_at = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
149
202
 
@@ -153,14 +206,25 @@ module Sidekiq
153
206
 
154
207
  is_interrupted = interrupted?
155
208
  if ::Process.clock_gettime(::Process::CLOCK_MONOTONIC) - state_flushed_at >= STATE_FLUSH_INTERVAL || is_interrupted
156
- flush_state
209
+ _, _, cancelled = flush_state
157
210
  state_flushed_at = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
211
+ if cancelled
212
+ @_cancelled = true
213
+ on_cancel
214
+ logger.info { "Job cancelled" }
215
+ return true
216
+ end
158
217
  end
159
218
 
160
219
  return false if is_interrupted
161
220
 
162
- around_iteration do
163
- each_iteration(object, *arguments)
221
+ verify_iteration_time(time_limit, object) do
222
+ around_iteration do
223
+ each_iteration(object, *arguments)
224
+ rescue Exception
225
+ flush_state
226
+ raise
227
+ end
164
228
  end
165
229
  end
166
230
 
@@ -170,6 +234,16 @@ module Sidekiq
170
234
  @_runtime += (::Process.clock_gettime(::Process::CLOCK_MONOTONIC) - @_start_time)
171
235
  end
172
236
 
237
+ def verify_iteration_time(time_limit, object)
238
+ start = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
239
+ yield
240
+ finish = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
241
+ total = finish - start
242
+ if total > time_limit
243
+ logger.warn { "Iteration took longer (%.2f) than Sidekiq's shutdown timeout (%d) when processing `%s`. This can lead to job processing problems during deploys" % [total, time_limit, object] }
244
+ end
245
+ end
246
+
173
247
  def reenqueue_iteration_job
174
248
  flush_state
175
249
  logger.debug { "Interrupting job (cursor=#{@_cursor.inspect})" }
@@ -203,7 +277,8 @@ module Sidekiq
203
277
  Sidekiq.redis do |conn|
204
278
  conn.multi do |pipe|
205
279
  pipe.hset(key, state)
206
- pipe.expire(key, STATE_TTL)
280
+ pipe.expire(key, STATE_TTL, "nx")
281
+ pipe.hget(key, "cancelled")
207
282
  end
208
283
  end
209
284
  end
@@ -2,52 +2,40 @@
2
2
 
3
3
  module Sidekiq
4
4
  class JobLogger
5
- include Sidekiq::Component
6
-
7
5
  def initialize(config)
8
6
  @config = config
9
- @logger = logger
10
- end
11
-
12
- # If true we won't do any job logging out of the box.
13
- # The user is responsible for any logging.
14
- def skip_default_logging?
15
- config[:skip_default_job_logging]
7
+ @logger = @config.logger
8
+ @skip = !!@config[:skip_default_job_logging]
16
9
  end
17
10
 
18
11
  def call(item, queue)
19
- return yield if skip_default_logging?
12
+ start = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
13
+ @logger.info { "start" } unless @skip
20
14
 
21
- begin
22
- start = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
23
- @logger.info("start")
15
+ yield
24
16
 
25
- yield
26
-
27
- Sidekiq::Context.add(:elapsed, elapsed(start))
28
- @logger.info("done")
29
- rescue Exception
30
- Sidekiq::Context.add(:elapsed, elapsed(start))
31
- @logger.info("fail")
32
-
33
- raise
34
- end
17
+ Sidekiq::Context.add(:elapsed, elapsed(start))
18
+ @logger.info { "done" } unless @skip
19
+ rescue Exception
20
+ Sidekiq::Context.add(:elapsed, elapsed(start))
21
+ @logger.info { "fail" } unless @skip
22
+ raise
35
23
  end
36
24
 
37
25
  def prepare(job_hash, &block)
38
26
  # If we're using a wrapper class, like ActiveJob, use the "wrapped"
39
27
  # attribute to expose the underlying thing.
40
28
  h = {
41
- class: job_hash["display_class"] || job_hash["wrapped"] || job_hash["class"],
42
- jid: job_hash["jid"]
29
+ jid: job_hash["jid"],
30
+ class: job_hash["display_class"] || job_hash["wrapped"] || job_hash["class"]
43
31
  }
44
32
  h[:bid] = job_hash["bid"] if job_hash.has_key?("bid")
45
33
  h[:tags] = job_hash["tags"] if job_hash.has_key?("tags")
46
34
 
47
35
  Thread.current[:sidekiq_context] = h
48
36
  level = job_hash["log_level"]
49
- if level && @logger.respond_to?(:log_at)
50
- @logger.log_at(level, &block)
37
+ if level
38
+ @logger.with_level(level, &block)
51
39
  else
52
40
  yield
53
41
  end
@@ -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
 
@@ -177,7 +181,7 @@ module Sidekiq
177
181
  return retries_exhausted(jobinst, msg, exception) if count >= max_retry_attempts
178
182
 
179
183
  rf = msg["retry_for"]
180
- return retries_exhausted(jobinst, msg, exception) if rf && ((msg["failed_at"] + rf) < Time.now.to_f)
184
+ return retries_exhausted(jobinst, msg, exception) if rf && (time_for(msg["failed_at"]) + rf) < Time.now
181
185
 
182
186
  strategy, delay = delay_for(jobinst, count, exception, msg)
183
187
  case strategy
@@ -189,7 +193,7 @@ module Sidekiq
189
193
 
190
194
  # Logging here can break retries if the logging device raises ENOSPC #3979
191
195
  # logger.debug { "Failure! Retry #{count} in #{delay} seconds" }
192
- jitter = rand(10) * (count + 1)
196
+ jitter = rand(10 * (count + 1))
193
197
  retry_at = Time.now.to_f + delay + jitter
194
198
  payload = Sidekiq.dump_json(msg)
195
199
  redis do |conn|
@@ -197,6 +201,14 @@ module Sidekiq
197
201
  end
198
202
  end
199
203
 
204
+ def time_for(item)
205
+ if item.is_a?(Float)
206
+ Time.at(item)
207
+ else
208
+ Time.at(item / 1000, item % 1000)
209
+ end
210
+ end
211
+
200
212
  # returns (strategy, seconds)
201
213
  def delay_for(jobinst, count, exception, msg)
202
214
  rv = begin
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "securerandom"
2
4
  require "time"
3
5
 
@@ -56,10 +58,14 @@ module Sidekiq
56
58
  item["class"] = item["class"].to_s
57
59
  item["queue"] = item["queue"].to_s
58
60
  item["retry_for"] = item["retry_for"].to_i if item["retry_for"]
59
- item["created_at"] ||= Time.now.to_f
61
+ item["created_at"] ||= now_in_millis
60
62
  item
61
63
  end
62
64
 
65
+ def now_in_millis
66
+ ::Process.clock_gettime(::Process::CLOCK_REALTIME, :millisecond)
67
+ end
68
+
63
69
  def normalized_hash(item_class)
64
70
  if item_class.is_a?(Class)
65
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)
@@ -36,8 +36,8 @@ module Sidekiq
36
36
  # has a heartbeat thread, caller can use `async_beat: false`
37
37
  # and instead have thread call Launcher#heartbeat every N seconds.
38
38
  def run(async_beat: true)
39
- Sidekiq.freeze!
40
39
  logger.debug { @config.merge!({}) }
40
+ Sidekiq.freeze!
41
41
  @thread = safe_thread("heartbeat", &method(:start_heartbeat)) if async_beat
42
42
  @poller.start
43
43
  @managers.each(&:start)
@@ -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
 
@@ -22,92 +22,41 @@ 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
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
+ }
79
34
  class Base < ::Logger::Formatter
80
35
  def tid
81
36
  Thread.current["sidekiq_tid"] ||= (Thread.current.object_id ^ ::Process.pid).to_s(36)
82
37
  end
83
38
 
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
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(" ")}"
99
48
  end
100
49
  end
101
50
 
102
51
  class Pretty < Base
103
52
  def call(severity, time, program_name, message)
104
- "#{time.utc.iso8601(3)} pid=#{::Process.pid} tid=#{tid}#{format_context} #{severity}: #{message}\n"
53
+ "#{Formatters::COLORS[severity]} #{time.utc.iso8601(3)} pid=#{::Process.pid} tid=#{tid}#{format_context}: #{message}\n"
105
54
  end
106
55
  end
107
56
 
108
57
  class WithoutTimestamp < Pretty
109
58
  def call(severity, time, program_name, message)
110
- "pid=#{::Process.pid} tid=#{tid}#{format_context} #{severity}: #{message}\n"
59
+ "#{Formatters::COLORS[severity]} pid=#{::Process.pid} tid=#{tid} #{format_context}: #{message}\n"
111
60
  end
112
61
  end
113
62
 
@@ -120,7 +69,7 @@ module Sidekiq
120
69
  lvl: severity,
121
70
  msg: message
122
71
  }
123
- c = ctx
72
+ c = Sidekiq::Context.current
124
73
  hash["ctx"] = c unless c.empty?
125
74
 
126
75
  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
  ##