sidekiq 6.5.1 → 7.3.6

Sign up to get free protection for your applications and to get access to all the features.
Files changed (125) hide show
  1. checksums.yaml +4 -4
  2. data/Changes.md +376 -12
  3. data/README.md +43 -35
  4. data/bin/multi_queue_bench +271 -0
  5. data/bin/sidekiq +3 -8
  6. data/bin/sidekiqload +213 -118
  7. data/bin/sidekiqmon +3 -0
  8. data/lib/active_job/queue_adapters/sidekiq_adapter.rb +88 -0
  9. data/lib/generators/sidekiq/job_generator.rb +2 -0
  10. data/lib/sidekiq/api.rb +378 -173
  11. data/lib/sidekiq/capsule.rb +132 -0
  12. data/lib/sidekiq/cli.rb +61 -63
  13. data/lib/sidekiq/client.rb +89 -40
  14. data/lib/sidekiq/component.rb +6 -2
  15. data/lib/sidekiq/config.rb +305 -0
  16. data/lib/sidekiq/deploy.rb +64 -0
  17. data/lib/sidekiq/embedded.rb +63 -0
  18. data/lib/sidekiq/fetch.rb +11 -14
  19. data/lib/sidekiq/iterable_job.rb +55 -0
  20. data/lib/sidekiq/job/interrupt_handler.rb +24 -0
  21. data/lib/sidekiq/job/iterable/active_record_enumerator.rb +53 -0
  22. data/lib/sidekiq/job/iterable/csv_enumerator.rb +47 -0
  23. data/lib/sidekiq/job/iterable/enumerators.rb +135 -0
  24. data/lib/sidekiq/job/iterable.rb +294 -0
  25. data/lib/sidekiq/job.rb +382 -10
  26. data/lib/sidekiq/job_logger.rb +8 -7
  27. data/lib/sidekiq/job_retry.rb +89 -46
  28. data/lib/sidekiq/job_util.rb +53 -15
  29. data/lib/sidekiq/launcher.rb +77 -69
  30. data/lib/sidekiq/logger.rb +2 -27
  31. data/lib/sidekiq/manager.rb +9 -11
  32. data/lib/sidekiq/metrics/query.rb +158 -0
  33. data/lib/sidekiq/metrics/shared.rb +106 -0
  34. data/lib/sidekiq/metrics/tracking.rb +148 -0
  35. data/lib/sidekiq/middleware/chain.rb +84 -48
  36. data/lib/sidekiq/middleware/current_attributes.rb +87 -20
  37. data/lib/sidekiq/middleware/modules.rb +2 -0
  38. data/lib/sidekiq/monitor.rb +19 -5
  39. data/lib/sidekiq/paginator.rb +11 -3
  40. data/lib/sidekiq/processor.rb +67 -56
  41. data/lib/sidekiq/rails.rb +22 -16
  42. data/lib/sidekiq/redis_client_adapter.rb +31 -71
  43. data/lib/sidekiq/redis_connection.rb +44 -117
  44. data/lib/sidekiq/ring_buffer.rb +2 -0
  45. data/lib/sidekiq/scheduled.rb +62 -35
  46. data/lib/sidekiq/systemd.rb +2 -0
  47. data/lib/sidekiq/testing.rb +37 -46
  48. data/lib/sidekiq/transaction_aware_client.rb +11 -5
  49. data/lib/sidekiq/version.rb +6 -1
  50. data/lib/sidekiq/web/action.rb +15 -5
  51. data/lib/sidekiq/web/application.rb +94 -24
  52. data/lib/sidekiq/web/csrf_protection.rb +10 -7
  53. data/lib/sidekiq/web/helpers.rb +118 -45
  54. data/lib/sidekiq/web/router.rb +5 -2
  55. data/lib/sidekiq/web.rb +67 -15
  56. data/lib/sidekiq/worker_compatibility_alias.rb +13 -0
  57. data/lib/sidekiq.rb +78 -266
  58. data/sidekiq.gemspec +12 -10
  59. data/web/assets/javascripts/application.js +46 -1
  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 +192 -0
  64. data/web/assets/javascripts/dashboard.js +11 -250
  65. data/web/assets/javascripts/metrics.js +298 -0
  66. data/web/assets/stylesheets/application-dark.css +4 -0
  67. data/web/assets/stylesheets/application-rtl.css +10 -89
  68. data/web/assets/stylesheets/application.css +98 -295
  69. data/web/locales/ar.yml +70 -70
  70. data/web/locales/cs.yml +62 -62
  71. data/web/locales/da.yml +60 -53
  72. data/web/locales/de.yml +65 -65
  73. data/web/locales/el.yml +43 -24
  74. data/web/locales/en.yml +83 -69
  75. data/web/locales/es.yml +68 -68
  76. data/web/locales/fa.yml +65 -65
  77. data/web/locales/fr.yml +80 -67
  78. data/web/locales/gd.yml +98 -0
  79. data/web/locales/he.yml +65 -64
  80. data/web/locales/hi.yml +59 -59
  81. data/web/locales/it.yml +85 -54
  82. data/web/locales/ja.yml +72 -68
  83. data/web/locales/ko.yml +52 -52
  84. data/web/locales/lt.yml +66 -66
  85. data/web/locales/nb.yml +61 -61
  86. data/web/locales/nl.yml +52 -52
  87. data/web/locales/pl.yml +45 -45
  88. data/web/locales/pt-br.yml +78 -69
  89. data/web/locales/pt.yml +51 -51
  90. data/web/locales/ru.yml +67 -66
  91. data/web/locales/sv.yml +53 -53
  92. data/web/locales/ta.yml +60 -60
  93. data/web/locales/tr.yml +100 -0
  94. data/web/locales/uk.yml +85 -61
  95. data/web/locales/ur.yml +64 -64
  96. data/web/locales/vi.yml +67 -67
  97. data/web/locales/zh-cn.yml +42 -16
  98. data/web/locales/zh-tw.yml +41 -8
  99. data/web/views/_footer.erb +17 -2
  100. data/web/views/_job_info.erb +18 -2
  101. data/web/views/_metrics_period_select.erb +12 -0
  102. data/web/views/_nav.erb +1 -1
  103. data/web/views/_paging.erb +2 -0
  104. data/web/views/_poll_link.erb +1 -1
  105. data/web/views/_summary.erb +7 -7
  106. data/web/views/busy.erb +49 -33
  107. data/web/views/dashboard.erb +28 -6
  108. data/web/views/filtering.erb +6 -0
  109. data/web/views/layout.erb +6 -6
  110. data/web/views/metrics.erb +90 -0
  111. data/web/views/metrics_for_job.erb +59 -0
  112. data/web/views/morgue.erb +5 -9
  113. data/web/views/queue.erb +15 -15
  114. data/web/views/queues.erb +9 -3
  115. data/web/views/retries.erb +5 -9
  116. data/web/views/scheduled.erb +12 -13
  117. metadata +61 -26
  118. data/lib/sidekiq/.DS_Store +0 -0
  119. data/lib/sidekiq/delay.rb +0 -43
  120. data/lib/sidekiq/extensions/action_mailer.rb +0 -48
  121. data/lib/sidekiq/extensions/active_record.rb +0 -43
  122. data/lib/sidekiq/extensions/class_methods.rb +0 -43
  123. data/lib/sidekiq/extensions/generic_proxy.rb +0 -33
  124. data/lib/sidekiq/worker.rb +0 -367
  125. /data/{LICENSE → LICENSE.txt} +0 -0
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "securerandom"
2
4
  require "time"
3
5
 
@@ -9,26 +11,32 @@ module Sidekiq
9
11
 
10
12
  def validate(item)
11
13
  raise(ArgumentError, "Job must be a Hash with 'class' and 'args' keys: `#{item}`") unless item.is_a?(Hash) && item.key?("class") && item.key?("args")
12
- raise(ArgumentError, "Job args must be an Array: `#{item}`") unless item["args"].is_a?(Array)
14
+ raise(ArgumentError, "Job args must be an Array: `#{item}`") unless item["args"].is_a?(Array) || item["args"].is_a?(Enumerator::Lazy)
13
15
  raise(ArgumentError, "Job class must be either a Class or String representation of the class name: `#{item}`") unless item["class"].is_a?(Class) || item["class"].is_a?(String)
14
16
  raise(ArgumentError, "Job 'at' must be a Numeric timestamp: `#{item}`") if item.key?("at") && !item["at"].is_a?(Numeric)
15
17
  raise(ArgumentError, "Job tags must be an Array: `#{item}`") if item["tags"] && !item["tags"].is_a?(Array)
18
+ raise(ArgumentError, "retry_for must be a relative amount of time, e.g. 48.hours `#{item}`") if item["retry_for"] && item["retry_for"] > 1_000_000_000
16
19
  end
17
20
 
18
21
  def verify_json(item)
19
22
  job_class = item["wrapped"] || item["class"]
20
- if Sidekiq[:on_complex_arguments] == :raise
21
- msg = <<~EOM
22
- Job arguments to #{job_class} must be native JSON types, see https://github.com/mperham/sidekiq/wiki/Best-Practices.
23
- To disable this error, remove `Sidekiq.strict_args!` from your initializer.
24
- EOM
25
- raise(ArgumentError, msg) unless json_safe?(item)
26
- elsif Sidekiq[:on_complex_arguments] == :warn
27
- Sidekiq.logger.warn <<~EOM unless json_safe?(item)
28
- Job arguments to #{job_class} do not serialize to JSON safely. This will raise an error in
29
- Sidekiq 7.0. See https://github.com/mperham/sidekiq/wiki/Best-Practices or raise an error today
30
- by calling `Sidekiq.strict_args!` during Sidekiq initialization.
31
- EOM
23
+ args = item["args"]
24
+ mode = Sidekiq::Config::DEFAULTS[:on_complex_arguments]
25
+
26
+ if mode == :raise || mode == :warn
27
+ if (unsafe_item = json_unsafe?(args))
28
+ msg = <<~EOM
29
+ Job arguments to #{job_class} must be native JSON types, but #{unsafe_item.inspect} is a #{unsafe_item.class}.
30
+ See https://github.com/sidekiq/sidekiq/wiki/Best-Practices
31
+ To disable this error, add `Sidekiq.strict_args!(false)` to your initializer.
32
+ EOM
33
+
34
+ if mode == :raise
35
+ raise(ArgumentError, msg)
36
+ else
37
+ warn(msg)
38
+ end
39
+ end
32
40
  end
33
41
  end
34
42
 
@@ -49,6 +57,7 @@ module Sidekiq
49
57
  item["jid"] ||= SecureRandom.hex(12)
50
58
  item["class"] = item["class"].to_s
51
59
  item["queue"] = item["queue"].to_s
60
+ item["retry_for"] = item["retry_for"].to_i if item["retry_for"]
52
61
  item["created_at"] ||= Time.now.to_f
53
62
  item
54
63
  end
@@ -64,8 +73,37 @@ module Sidekiq
64
73
 
65
74
  private
66
75
 
67
- def json_safe?(item)
68
- JSON.parse(JSON.dump(item["args"])) == item["args"]
76
+ RECURSIVE_JSON_UNSAFE = {
77
+ Integer => ->(val) {},
78
+ Float => ->(val) {},
79
+ TrueClass => ->(val) {},
80
+ FalseClass => ->(val) {},
81
+ NilClass => ->(val) {},
82
+ String => ->(val) {},
83
+ Array => ->(val) {
84
+ val.each do |e|
85
+ unsafe_item = RECURSIVE_JSON_UNSAFE[e.class].call(e)
86
+ return unsafe_item unless unsafe_item.nil?
87
+ end
88
+ nil
89
+ },
90
+ Hash => ->(val) {
91
+ val.each do |k, v|
92
+ return k unless String === k
93
+
94
+ unsafe_item = RECURSIVE_JSON_UNSAFE[v.class].call(v)
95
+ return unsafe_item unless unsafe_item.nil?
96
+ end
97
+ nil
98
+ }
99
+ }
100
+
101
+ RECURSIVE_JSON_UNSAFE.default = ->(val) { val }
102
+ RECURSIVE_JSON_UNSAFE.compare_by_identity
103
+ private_constant :RECURSIVE_JSON_UNSAFE
104
+
105
+ def json_unsafe?(item)
106
+ RECURSIVE_JSON_UNSAFE[item.class].call(item)
69
107
  end
70
108
  end
71
109
  end
@@ -1,12 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "sidekiq/manager"
4
- require "sidekiq/fetch"
4
+ require "sidekiq/capsule"
5
5
  require "sidekiq/scheduled"
6
6
  require "sidekiq/ring_buffer"
7
7
 
8
8
  module Sidekiq
9
- # The Launcher starts the Manager and Poller threads and provides the process heartbeat.
9
+ # The Launcher starts the Capsule Managers, the Poller thread and provides the process heartbeat.
10
10
  class Launcher
11
11
  include Sidekiq::Component
12
12
 
@@ -16,48 +16,56 @@ module Sidekiq
16
16
  proc { "sidekiq" },
17
17
  proc { Sidekiq::VERSION },
18
18
  proc { |me, data| data["tag"] },
19
- proc { |me, data| "[#{Processor::WORK_STATE.size} of #{data["concurrency"]} busy]" },
19
+ proc { |me, data| "[#{Processor::WORK_STATE.size} of #{me.config.total_concurrency} busy]" },
20
20
  proc { |me, data| "stopping" if me.stopping? }
21
21
  ]
22
22
 
23
- attr_accessor :manager, :poller, :fetcher
23
+ attr_accessor :managers, :poller
24
24
 
25
- def initialize(options)
26
- @config = options
27
- options[:fetch] ||= BasicFetch.new(options)
28
- @manager = Sidekiq::Manager.new(options)
29
- @poller = Sidekiq::Scheduled::Poller.new(options)
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)
30
32
  @done = false
31
33
  end
32
34
 
33
- def run
34
- @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
35
42
  @poller.start
36
- @manager.start
43
+ @managers.each(&:start)
37
44
  end
38
45
 
39
46
  # Stops this instance from processing any more jobs,
40
- #
41
47
  def quiet
48
+ return if @done
49
+
42
50
  @done = true
43
- @manager.quiet
51
+ @managers.each(&:quiet)
44
52
  @poller.terminate
53
+ fire_event(:quiet, reverse: true)
45
54
  end
46
55
 
47
56
  # Shuts down this Sidekiq instance. Waits up to the deadline for all jobs to complete.
48
57
  def stop
49
58
  deadline = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC) + @config[:timeout]
50
59
 
51
- @done = true
52
- @manager.quiet
53
- @poller.terminate
54
-
55
- @manager.stop(deadline)
60
+ quiet
61
+ stoppers = @managers.map do |mgr|
62
+ Thread.new do
63
+ mgr.stop(deadline)
64
+ end
65
+ end
56
66
 
57
- # Requeue everything in case there was a thread which fetched a job while the process was stopped.
58
- # This call is a no-op in Sidekiq but necessary for Sidekiq Pro.
59
- strategy = @config[:fetch]
60
- strategy.bulk_requeue([], @config)
67
+ fire_event(:shutdown, reverse: true)
68
+ stoppers.each(&:join)
61
69
 
62
70
  clear_heartbeat
63
71
  end
@@ -66,25 +74,39 @@ module Sidekiq
66
74
  @done
67
75
  end
68
76
 
77
+ # If embedding Sidekiq, you can have the process heartbeat
78
+ # call this method to regularly heartbeat rather than creating
79
+ # a separate thread.
80
+ def heartbeat
81
+
82
+ end
83
+
69
84
  private unless $TESTING
70
85
 
71
- BEAT_PAUSE = 5
86
+ BEAT_PAUSE = 10
72
87
 
73
88
  def start_heartbeat
74
89
  loop do
75
- heartbeat
90
+ beat
76
91
  sleep BEAT_PAUSE
77
92
  end
78
93
  logger.info("Heartbeat stopping...")
79
94
  end
80
95
 
96
+ def beat
97
+ $0 = PROCTITLES.map { |proc| proc.call(self, to_data) }.compact.join(" ") unless @embedded
98
+
99
+ end
100
+
81
101
  def clear_heartbeat
102
+ flush_stats
103
+
82
104
  # Remove record from Redis since we are shutting down.
83
105
  # Note we don't stop the heartbeat thread; if the process
84
106
  # doesn't actually exit, it'll reappear in the Web UI.
85
107
  redis do |conn|
86
108
  conn.pipelined do |pipeline|
87
- pipeline.srem("processes", identity)
109
+ pipeline.srem("processes", [identity])
88
110
  pipeline.unlink("#{identity}:work")
89
111
  end
90
112
  end
@@ -92,20 +114,14 @@ module Sidekiq
92
114
  # best effort, ignore network errors
93
115
  end
94
116
 
95
- def heartbeat
96
- $0 = PROCTITLES.map { |proc| proc.call(self, to_data) }.compact.join(" ")
97
-
98
-
99
- end
100
-
101
- def self.flush_stats
117
+ def flush_stats
102
118
  fails = Processor::FAILURE.reset
103
119
  procd = Processor::PROCESSED.reset
104
120
  return if fails + procd == 0
105
121
 
106
122
  nowdate = Time.now.utc.strftime("%Y-%m-%d")
107
123
  begin
108
- Sidekiq.redis do |conn|
124
+ redis do |conn|
109
125
  conn.pipelined do |pipeline|
110
126
  pipeline.incrby("stat:processed", procd)
111
127
  pipeline.incrby("stat:processed:#{nowdate}", procd)
@@ -117,43 +133,29 @@ module Sidekiq
117
133
  end
118
134
  end
119
135
  rescue => ex
120
- # we're exiting the process, things might be shut down so don't
121
- # try to handle the exception
122
- Sidekiq.logger.warn("Unable to flush stats: #{ex}")
136
+ logger.warn("Unable to flush stats: #{ex}")
123
137
  end
124
138
  end
125
- at_exit(&method(:flush_stats))
126
139
 
127
140
  def ❤
128
141
  key = identity
129
142
  fails = procd = 0
130
143
 
131
144
  begin
132
- fails = Processor::FAILURE.reset
133
- procd = Processor::PROCESSED.reset
134
- curstate = Processor::WORK_STATE.dup
145
+ flush_stats
135
146
 
136
- nowdate = Time.now.utc.strftime("%Y-%m-%d")
147
+ curstate = Processor::WORK_STATE.dup
148
+ curstate.transform_values! { |val| Sidekiq.dump_json(val) }
137
149
 
138
150
  redis do |conn|
139
- conn.multi do |transaction|
140
- transaction.incrby("stat:processed", procd)
141
- transaction.incrby("stat:processed:#{nowdate}", procd)
142
- transaction.expire("stat:processed:#{nowdate}", STATS_TTL)
143
-
144
- transaction.incrby("stat:failed", fails)
145
- transaction.incrby("stat:failed:#{nowdate}", fails)
146
- transaction.expire("stat:failed:#{nowdate}", STATS_TTL)
147
- end
148
-
149
151
  # work is the current set of executing jobs
150
152
  work_key = "#{key}:work"
151
- conn.pipelined do |transaction|
153
+ conn.multi do |transaction|
152
154
  transaction.unlink(work_key)
153
- curstate.each_pair do |tid, hash|
154
- transaction.hset(work_key, tid, Sidekiq.dump_json(hash))
155
+ if curstate.size > 0
156
+ transaction.hset(work_key, curstate)
157
+ transaction.expire(work_key, 60)
155
158
  end
156
- transaction.expire(work_key, 60)
157
159
  end
158
160
  end
159
161
 
@@ -162,11 +164,11 @@ module Sidekiq
162
164
  fails = procd = 0
163
165
  kb = memory_usage(::Process.pid)
164
166
 
165
- _, exists, _, _, msg = redis { |conn|
167
+ _, exists, _, _, signal = redis { |conn|
166
168
  conn.multi { |transaction|
167
- transaction.sadd("processes", key)
168
- transaction.exists?(key)
169
- transaction.hmset(key, "info", to_json,
169
+ transaction.sadd("processes", [key])
170
+ transaction.exists(key)
171
+ transaction.hset(key, "info", to_json,
170
172
  "busy", curstate.size,
171
173
  "beat", Time.now.to_f,
172
174
  "rtt_us", rtt,
@@ -178,11 +180,10 @@ module Sidekiq
178
180
  }
179
181
 
180
182
  # first heartbeat or recovering from an outage and need to reestablish our heartbeat
181
- fire_event(:heartbeat) unless exists
183
+ fire_event(:heartbeat) unless exists > 0
184
+ fire_event(:beat, oneshot: false)
182
185
 
183
- return unless msg
184
-
185
- ::Process.kill(msg, ::Process.pid)
186
+ ::Process.kill(signal, ::Process.pid) if signal && !@embedded
186
187
  rescue => e
187
188
  # ignore all redis/network issues
188
189
  logger.error("heartbeat: #{e}")
@@ -216,7 +217,7 @@ module Sidekiq
216
217
  Last RTT readings were #{RTT_READINGS.buffer.inspect}, ideally these should be < 1000.
217
218
  Ensure Redis is running in the same AZ or datacenter as Sidekiq.
218
219
  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
+ CPU-saturated; reduce your concurrency and/or see https://github.com/sidekiq/sidekiq/discussions/5039
220
221
  EOM
221
222
  RTT_READINGS.reset
222
223
  end
@@ -249,13 +250,20 @@ module Sidekiq
249
250
  "started_at" => Time.now.to_f,
250
251
  "pid" => ::Process.pid,
251
252
  "tag" => @config[:tag] || "",
252
- "concurrency" => @config[:concurrency],
253
- "queues" => @config[:queues].uniq,
254
- "labels" => @config[:labels],
255
- "identity" => identity
253
+ "concurrency" => @config.total_concurrency,
254
+ "queues" => @config.capsules.values.flat_map { |cap| cap.queues }.uniq,
255
+ "weights" => to_weights,
256
+ "labels" => @config[:labels].to_a,
257
+ "identity" => identity,
258
+ "version" => Sidekiq::VERSION,
259
+ "embedded" => @embedded
256
260
  }
257
261
  end
258
262
 
263
+ def to_weights
264
+ @config.capsules.values.map(&:weights)
265
+ end
266
+
259
267
  def to_json
260
268
  # this data changes infrequently so dump it to a string
261
269
  # now so we don't need to dump it every heartbeat.
@@ -31,12 +31,12 @@ module Sidekiq
31
31
  "fatal" => 4
32
32
  }
33
33
  LEVELS.default_proc = proc do |_, level|
34
- Sidekiq.logger.warn("Invalid log level: #{level.inspect}")
34
+ puts("Invalid log level: #{level.inspect}")
35
35
  nil
36
36
  end
37
37
 
38
38
  LEVELS.each do |level, numeric_level|
39
- define_method("#{level}?") do
39
+ define_method(:"#{level}?") do
40
40
  local_level.nil? ? super() : local_level <= numeric_level
41
41
  end
42
42
  end
@@ -70,36 +70,11 @@ module Sidekiq
70
70
  ensure
71
71
  self.local_level = old_local_level
72
72
  end
73
-
74
- # Redefined to check severity against #level, and thus the thread-local level, rather than +@level+.
75
- # FIXME: Remove when the minimum Ruby version supports overriding Logger#level.
76
- def add(severity, message = nil, progname = nil, &block)
77
- severity ||= ::Logger::UNKNOWN
78
- progname ||= @progname
79
-
80
- return true if @logdev.nil? || severity < level
81
-
82
- if message.nil?
83
- if block
84
- message = yield
85
- else
86
- message = progname
87
- progname = @progname
88
- end
89
- end
90
-
91
- @logdev.write format_message(format_severity(severity), Time.now, progname, message)
92
- end
93
73
  end
94
74
 
95
75
  class Logger < ::Logger
96
76
  include LoggingUtils
97
77
 
98
- def initialize(*args, **kwargs)
99
- super
100
- self.formatter = Sidekiq.log_formatter
101
- end
102
-
103
78
  module Formatters
104
79
  class Base < ::Logger::Formatter
105
80
  def tid
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "sidekiq/processor"
4
- require "sidekiq/fetch"
5
4
  require "set"
6
5
 
7
6
  module Sidekiq
@@ -23,19 +22,19 @@ module Sidekiq
23
22
  include Sidekiq::Component
24
23
 
25
24
  attr_reader :workers
25
+ attr_reader :capsule
26
26
 
27
- def initialize(options = {})
28
- @config = options
29
- logger.debug { options.inspect }
30
- @count = options[:concurrency] || 10
27
+ def initialize(capsule)
28
+ @config = @capsule = capsule
29
+ @count = capsule.concurrency
31
30
  raise ArgumentError, "Concurrency of #{@count} is not supported" if @count < 1
32
31
 
33
32
  @done = false
34
33
  @workers = Set.new
34
+ @plock = Mutex.new
35
35
  @count.times do
36
36
  @workers << Processor.new(@config, &method(:processor_result))
37
37
  end
38
- @plock = Mutex.new
39
38
  end
40
39
 
41
40
  def start
@@ -46,14 +45,12 @@ module Sidekiq
46
45
  return if @done
47
46
  @done = true
48
47
 
49
- logger.info { "Terminating quiet threads" }
48
+ logger.info { "Terminating quiet threads for #{capsule.name} capsule" }
50
49
  @workers.each(&:terminate)
51
- fire_event(:quiet, reverse: true)
52
50
  end
53
51
 
54
52
  def stop(deadline)
55
53
  quiet
56
- fire_event(:shutdown, reverse: true)
57
54
 
58
55
  # some of the shutdown events can be async,
59
56
  # we don't have any way to know when they're done but
@@ -66,6 +63,8 @@ module Sidekiq
66
63
  return if @workers.empty?
67
64
 
68
65
  hard_shutdown
66
+ ensure
67
+ capsule.stop
69
68
  end
70
69
 
71
70
  def processor_result(processor, reason = nil)
@@ -105,8 +104,7 @@ module Sidekiq
105
104
  # contract says that jobs are run AT LEAST once. Process termination
106
105
  # is delayed until we're certain the jobs are back in Redis because
107
106
  # it is worse to lose a job than to run it twice.
108
- strategy = @config[:fetch]
109
- strategy.bulk_requeue(jobs, @config)
107
+ capsule.fetcher.bulk_requeue(jobs)
110
108
  end
111
109
 
112
110
  cleanup.each do |processor|
@@ -0,0 +1,158 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "sidekiq"
4
+ require "date"
5
+ require "set"
6
+
7
+ require "sidekiq/metrics/shared"
8
+
9
+ module Sidekiq
10
+ module Metrics
11
+ # Allows caller to query for Sidekiq execution metrics within Redis.
12
+ # Caller sets a set of attributes to act as filters. {#fetch} will call
13
+ # Redis and return a Hash of results.
14
+ #
15
+ # NB: all metrics and times/dates are UTC only. We specifically do not
16
+ # support timezones.
17
+ class Query
18
+ def initialize(pool: nil, now: Time.now)
19
+ @time = now.utc
20
+ @pool = pool || Sidekiq.default_configuration.redis_pool
21
+ @klass = nil
22
+ end
23
+
24
+ # Get metric data for all jobs from the last hour
25
+ # +class_filter+: return only results for classes matching filter
26
+ def top_jobs(class_filter: nil, minutes: 60)
27
+ result = Result.new
28
+
29
+ time = @time
30
+ redis_results = @pool.with do |conn|
31
+ conn.pipelined do |pipe|
32
+ minutes.times do |idx|
33
+ key = "j|#{time.strftime("%Y%m%d")}|#{time.hour}:#{time.min}"
34
+ pipe.hgetall key
35
+ result.prepend_bucket time
36
+ time -= 60
37
+ end
38
+ end
39
+ end
40
+
41
+ time = @time
42
+ redis_results.each do |hash|
43
+ hash.each do |k, v|
44
+ kls, metric = k.split("|")
45
+ next if class_filter && !class_filter.match?(kls)
46
+ result.job_results[kls].add_metric metric, time, v.to_i
47
+ end
48
+ time -= 60
49
+ end
50
+
51
+ result.marks = fetch_marks(result.starts_at..result.ends_at)
52
+
53
+ result
54
+ end
55
+
56
+ def for_job(klass, minutes: 60)
57
+ result = Result.new
58
+
59
+ time = @time
60
+ redis_results = @pool.with do |conn|
61
+ conn.pipelined do |pipe|
62
+ minutes.times do |idx|
63
+ key = "j|#{time.strftime("%Y%m%d")}|#{time.hour}:#{time.min}"
64
+ pipe.hmget key, "#{klass}|ms", "#{klass}|p", "#{klass}|f"
65
+ result.prepend_bucket time
66
+ time -= 60
67
+ end
68
+ end
69
+ end
70
+
71
+ time = @time
72
+ @pool.with do |conn|
73
+ redis_results.each do |(ms, p, f)|
74
+ result.job_results[klass].add_metric "ms", time, ms.to_i if ms
75
+ result.job_results[klass].add_metric "p", time, p.to_i if p
76
+ result.job_results[klass].add_metric "f", time, f.to_i if f
77
+ result.job_results[klass].add_hist time, Histogram.new(klass).fetch(conn, time).reverse
78
+ time -= 60
79
+ end
80
+ end
81
+
82
+ result.marks = fetch_marks(result.starts_at..result.ends_at)
83
+
84
+ result
85
+ end
86
+
87
+ class Result < Struct.new(:starts_at, :ends_at, :size, :buckets, :job_results, :marks)
88
+ def initialize
89
+ super
90
+ self.buckets = []
91
+ self.marks = []
92
+ self.job_results = Hash.new { |h, k| h[k] = JobResult.new }
93
+ end
94
+
95
+ def prepend_bucket(time)
96
+ buckets.unshift time.strftime("%H:%M")
97
+ self.ends_at ||= time
98
+ self.starts_at = time
99
+ end
100
+ end
101
+
102
+ class JobResult < Struct.new(:series, :hist, :totals)
103
+ def initialize
104
+ super
105
+ self.series = Hash.new { |h, k| h[k] = Hash.new(0) }
106
+ self.hist = Hash.new { |h, k| h[k] = [] }
107
+ self.totals = Hash.new(0)
108
+ end
109
+
110
+ def add_metric(metric, time, value)
111
+ totals[metric] += value
112
+ series[metric][time.strftime("%H:%M")] += value
113
+
114
+ # Include timing measurements in seconds for convenience
115
+ add_metric("s", time, value / 1000.0) if metric == "ms"
116
+ end
117
+
118
+ def add_hist(time, hist_result)
119
+ hist[time.strftime("%H:%M")] = hist_result
120
+ end
121
+
122
+ def total_avg(metric = "ms")
123
+ completed = totals["p"] - totals["f"]
124
+ return 0 if completed.zero?
125
+ totals[metric].to_f / completed
126
+ end
127
+
128
+ def series_avg(metric = "ms")
129
+ series[metric].each_with_object(Hash.new(0)) do |(bucket, value), result|
130
+ completed = series.dig("p", bucket) - series.dig("f", bucket)
131
+ result[bucket] = (completed == 0) ? 0 : value.to_f / completed
132
+ end
133
+ end
134
+ end
135
+
136
+ class MarkResult < Struct.new(:time, :label)
137
+ def bucket
138
+ time.strftime("%H:%M")
139
+ end
140
+ end
141
+
142
+ private
143
+
144
+ def fetch_marks(time_range)
145
+ [].tap do |result|
146
+ marks = @pool.with { |c| c.hgetall("#{@time.strftime("%Y%m%d")}-marks") }
147
+
148
+ marks.each do |timestamp, label|
149
+ time = Time.parse(timestamp)
150
+ if time_range.cover? time
151
+ result << MarkResult.new(time, label)
152
+ end
153
+ end
154
+ end
155
+ end
156
+ end
157
+ end
158
+ end