sidekiq 6.5.12 → 7.3.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (123) hide show
  1. checksums.yaml +4 -4
  2. data/Changes.md +303 -20
  3. data/README.md +43 -35
  4. data/bin/multi_queue_bench +271 -0
  5. data/bin/sidekiq +3 -8
  6. data/bin/sidekiqload +204 -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 +196 -138
  11. data/lib/sidekiq/capsule.rb +132 -0
  12. data/lib/sidekiq/cli.rb +60 -75
  13. data/lib/sidekiq/client.rb +87 -38
  14. data/lib/sidekiq/component.rb +4 -1
  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 +23 -12
  27. data/lib/sidekiq/job_retry.rb +42 -19
  28. data/lib/sidekiq/job_util.rb +53 -15
  29. data/lib/sidekiq/launcher.rb +71 -65
  30. data/lib/sidekiq/logger.rb +2 -27
  31. data/lib/sidekiq/manager.rb +9 -11
  32. data/lib/sidekiq/metrics/query.rb +9 -4
  33. data/lib/sidekiq/metrics/shared.rb +21 -9
  34. data/lib/sidekiq/metrics/tracking.rb +40 -26
  35. data/lib/sidekiq/middleware/chain.rb +19 -18
  36. data/lib/sidekiq/middleware/current_attributes.rb +70 -20
  37. data/lib/sidekiq/middleware/modules.rb +2 -0
  38. data/lib/sidekiq/monitor.rb +18 -4
  39. data/lib/sidekiq/paginator.rb +2 -2
  40. data/lib/sidekiq/processor.rb +62 -57
  41. data/lib/sidekiq/rails.rb +21 -10
  42. data/lib/sidekiq/redis_client_adapter.rb +31 -71
  43. data/lib/sidekiq/redis_connection.rb +44 -115
  44. data/lib/sidekiq/ring_buffer.rb +2 -0
  45. data/lib/sidekiq/scheduled.rb +22 -23
  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 +89 -17
  52. data/lib/sidekiq/web/csrf_protection.rb +10 -7
  53. data/lib/sidekiq/web/helpers.rb +102 -42
  54. data/lib/sidekiq/web/router.rb +5 -2
  55. data/lib/sidekiq/web.rb +65 -17
  56. data/lib/sidekiq/worker_compatibility_alias.rb +13 -0
  57. data/lib/sidekiq.rb +78 -274
  58. data/sidekiq.gemspec +12 -10
  59. data/web/assets/javascripts/application.js +44 -0
  60. data/web/assets/javascripts/base-charts.js +106 -0
  61. data/web/assets/javascripts/dashboard-charts.js +192 -0
  62. data/web/assets/javascripts/dashboard.js +11 -233
  63. data/web/assets/javascripts/metrics.js +151 -115
  64. data/web/assets/stylesheets/application-dark.css +4 -0
  65. data/web/assets/stylesheets/application-rtl.css +10 -89
  66. data/web/assets/stylesheets/application.css +53 -298
  67. data/web/locales/ar.yml +70 -70
  68. data/web/locales/cs.yml +62 -62
  69. data/web/locales/da.yml +60 -53
  70. data/web/locales/de.yml +65 -65
  71. data/web/locales/el.yml +2 -7
  72. data/web/locales/en.yml +78 -71
  73. data/web/locales/es.yml +68 -68
  74. data/web/locales/fa.yml +65 -65
  75. data/web/locales/fr.yml +80 -67
  76. data/web/locales/gd.yml +98 -0
  77. data/web/locales/he.yml +65 -64
  78. data/web/locales/hi.yml +59 -59
  79. data/web/locales/it.yml +53 -53
  80. data/web/locales/ja.yml +67 -70
  81. data/web/locales/ko.yml +52 -52
  82. data/web/locales/lt.yml +66 -66
  83. data/web/locales/nb.yml +61 -61
  84. data/web/locales/nl.yml +52 -52
  85. data/web/locales/pl.yml +45 -45
  86. data/web/locales/pt-br.yml +78 -69
  87. data/web/locales/pt.yml +51 -51
  88. data/web/locales/ru.yml +67 -66
  89. data/web/locales/sv.yml +53 -53
  90. data/web/locales/ta.yml +60 -60
  91. data/web/locales/tr.yml +100 -0
  92. data/web/locales/uk.yml +85 -61
  93. data/web/locales/ur.yml +64 -64
  94. data/web/locales/vi.yml +67 -67
  95. data/web/locales/zh-cn.yml +20 -19
  96. data/web/locales/zh-tw.yml +10 -2
  97. data/web/views/_footer.erb +17 -2
  98. data/web/views/_job_info.erb +18 -2
  99. data/web/views/_metrics_period_select.erb +12 -0
  100. data/web/views/_paging.erb +2 -0
  101. data/web/views/_poll_link.erb +1 -1
  102. data/web/views/_summary.erb +7 -7
  103. data/web/views/busy.erb +46 -35
  104. data/web/views/dashboard.erb +28 -7
  105. data/web/views/filtering.erb +7 -0
  106. data/web/views/layout.erb +6 -6
  107. data/web/views/metrics.erb +48 -26
  108. data/web/views/metrics_for_job.erb +43 -71
  109. data/web/views/morgue.erb +5 -9
  110. data/web/views/queue.erb +10 -14
  111. data/web/views/queues.erb +9 -3
  112. data/web/views/retries.erb +5 -9
  113. data/web/views/scheduled.erb +12 -13
  114. metadata +53 -39
  115. data/lib/sidekiq/delay.rb +0 -43
  116. data/lib/sidekiq/extensions/action_mailer.rb +0 -48
  117. data/lib/sidekiq/extensions/active_record.rb +0 -43
  118. data/lib/sidekiq/extensions/class_methods.rb +0 -43
  119. data/lib/sidekiq/extensions/generic_proxy.rb +0 -33
  120. data/lib/sidekiq/metrics/deploy.rb +0 -47
  121. data/lib/sidekiq/worker.rb +0 -370
  122. data/web/assets/javascripts/graph.js +0 -16
  123. /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,18 +74,30 @@ 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
82
102
  flush_stats
83
103
 
@@ -94,12 +114,6 @@ module Sidekiq
94
114
  # best effort, ignore network errors
95
115
  end
96
116
 
97
- def heartbeat
98
- $0 = PROCTITLES.map { |proc| proc.call(self, to_data) }.compact.join(" ")
99
-
100
-
101
- end
102
-
103
117
  def flush_stats
104
118
  fails = Processor::FAILURE.reset
105
119
  procd = Processor::PROCESSED.reset
@@ -107,7 +121,7 @@ module Sidekiq
107
121
 
108
122
  nowdate = Time.now.utc.strftime("%Y-%m-%d")
109
123
  begin
110
- Sidekiq.redis do |conn|
124
+ redis do |conn|
111
125
  conn.pipelined do |pipeline|
112
126
  pipeline.incrby("stat:processed", procd)
113
127
  pipeline.incrby("stat:processed:#{nowdate}", procd)
@@ -119,9 +133,7 @@ module Sidekiq
119
133
  end
120
134
  end
121
135
  rescue => ex
122
- # we're exiting the process, things might be shut down so don't
123
- # try to handle the exception
124
- Sidekiq.logger.warn("Unable to flush stats: #{ex}")
136
+ logger.warn("Unable to flush stats: #{ex}")
125
137
  end
126
138
  end
127
139
 
@@ -130,31 +142,20 @@ module Sidekiq
130
142
  fails = procd = 0
131
143
 
132
144
  begin
133
- fails = Processor::FAILURE.reset
134
- procd = Processor::PROCESSED.reset
135
- curstate = Processor::WORK_STATE.dup
145
+ flush_stats
136
146
 
137
- nowdate = Time.now.utc.strftime("%Y-%m-%d")
147
+ curstate = Processor::WORK_STATE.dup
148
+ curstate.transform_values! { |val| Sidekiq.dump_json(val) }
138
149
 
139
150
  redis do |conn|
140
- conn.multi do |transaction|
141
- transaction.incrby("stat:processed", procd)
142
- transaction.incrby("stat:processed:#{nowdate}", procd)
143
- transaction.expire("stat:processed:#{nowdate}", STATS_TTL)
144
-
145
- transaction.incrby("stat:failed", fails)
146
- transaction.incrby("stat:failed:#{nowdate}", fails)
147
- transaction.expire("stat:failed:#{nowdate}", STATS_TTL)
148
- end
149
-
150
151
  # work is the current set of executing jobs
151
152
  work_key = "#{key}:work"
152
- conn.pipelined do |transaction|
153
+ conn.multi do |transaction|
153
154
  transaction.unlink(work_key)
154
- curstate.each_pair do |tid, hash|
155
- 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)
156
158
  end
157
- transaction.expire(work_key, 60)
158
159
  end
159
160
  end
160
161
 
@@ -163,11 +164,11 @@ module Sidekiq
163
164
  fails = procd = 0
164
165
  kb = memory_usage(::Process.pid)
165
166
 
166
- _, exists, _, _, msg = redis { |conn|
167
+ _, exists, _, _, signal = redis { |conn|
167
168
  conn.multi { |transaction|
168
169
  transaction.sadd("processes", [key])
169
- transaction.exists?(key)
170
- transaction.hmset(key, "info", to_json,
170
+ transaction.exists(key)
171
+ transaction.hset(key, "info", to_json,
171
172
  "busy", curstate.size,
172
173
  "beat", Time.now.to_f,
173
174
  "rtt_us", rtt,
@@ -179,12 +180,10 @@ module Sidekiq
179
180
  }
180
181
 
181
182
  # first heartbeat or recovering from an outage and need to reestablish our heartbeat
182
- fire_event(:heartbeat) unless exists
183
+ fire_event(:heartbeat) unless exists > 0
183
184
  fire_event(:beat, oneshot: false)
184
185
 
185
- return unless msg
186
-
187
- ::Process.kill(msg, ::Process.pid)
186
+ ::Process.kill(signal, ::Process.pid) if signal && !@embedded
188
187
  rescue => e
189
188
  # ignore all redis/network issues
190
189
  logger.error("heartbeat: #{e}")
@@ -218,7 +217,7 @@ module Sidekiq
218
217
  Last RTT readings were #{RTT_READINGS.buffer.inspect}, ideally these should be < 1000.
219
218
  Ensure Redis is running in the same AZ or datacenter as Sidekiq.
220
219
  If these values are close to 100,000, that means your Sidekiq process may be
221
- CPU-saturated; reduce your concurrency and/or see https://github.com/mperham/sidekiq/discussions/5039
220
+ CPU-saturated; reduce your concurrency and/or see https://github.com/sidekiq/sidekiq/discussions/5039
222
221
  EOM
223
222
  RTT_READINGS.reset
224
223
  end
@@ -251,13 +250,20 @@ module Sidekiq
251
250
  "started_at" => Time.now.to_f,
252
251
  "pid" => ::Process.pid,
253
252
  "tag" => @config[:tag] || "",
254
- "concurrency" => @config[:concurrency],
255
- "queues" => @config[:queues].uniq,
256
- "labels" => @config[:labels],
257
- "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
258
260
  }
259
261
  end
260
262
 
263
+ def to_weights
264
+ @config.capsules.values.map(&:weights)
265
+ end
266
+
261
267
  def to_json
262
268
  # this data changes infrequently so dump it to a string
263
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|
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "sidekiq"
2
4
  require "date"
3
5
  require "set"
@@ -13,14 +15,15 @@ module Sidekiq
13
15
  # NB: all metrics and times/dates are UTC only. We specifically do not
14
16
  # support timezones.
15
17
  class Query
16
- def initialize(pool: Sidekiq.redis_pool, now: Time.now)
18
+ def initialize(pool: nil, now: Time.now)
17
19
  @time = now.utc
18
- @pool = pool
20
+ @pool = pool || Sidekiq.default_configuration.redis_pool
19
21
  @klass = nil
20
22
  end
21
23
 
22
24
  # Get metric data for all jobs from the last hour
23
- def top_jobs(minutes: 60)
25
+ # +class_filter+: return only results for classes matching filter
26
+ def top_jobs(class_filter: nil, minutes: 60)
24
27
  result = Result.new
25
28
 
26
29
  time = @time
@@ -39,6 +42,7 @@ module Sidekiq
39
42
  redis_results.each do |hash|
40
43
  hash.each do |k, v|
41
44
  kls, metric = k.split("|")
45
+ next if class_filter && !class_filter.match?(kls)
42
46
  result.job_results[kls].add_metric metric, time, v.to_i
43
47
  end
44
48
  time -= 60
@@ -70,7 +74,7 @@ module Sidekiq
70
74
  result.job_results[klass].add_metric "ms", time, ms.to_i if ms
71
75
  result.job_results[klass].add_metric "p", time, p.to_i if p
72
76
  result.job_results[klass].add_metric "f", time, f.to_i if f
73
- result.job_results[klass].add_hist time, Histogram.new(klass).fetch(conn, time)
77
+ result.job_results[klass].add_hist time, Histogram.new(klass).fetch(conn, time).reverse
74
78
  time -= 60
75
79
  end
76
80
  end
@@ -117,6 +121,7 @@ module Sidekiq
117
121
 
118
122
  def total_avg(metric = "ms")
119
123
  completed = totals["p"] - totals["f"]
124
+ return 0 if completed.zero?
120
125
  totals[metric].to_f / completed
121
126
  end
122
127
 
@@ -1,9 +1,21 @@
1
- require "concurrent"
1
+ # frozen_string_literal: true
2
2
 
3
3
  module Sidekiq
4
4
  module Metrics
5
- # TODO Support apps without concurrent-ruby
6
- Counter = ::Concurrent::AtomicFixnum
5
+ class Counter
6
+ def initialize
7
+ @value = 0
8
+ @lock = Mutex.new
9
+ end
10
+
11
+ def increment
12
+ @lock.synchronize { @value += 1 }
13
+ end
14
+
15
+ def value
16
+ @lock.synchronize { @value }
17
+ end
18
+ end
7
19
 
8
20
  # Implements space-efficient but statistically useful histogram storage.
9
21
  # A precise time histogram stores every time. Instead we break times into a set of
@@ -28,8 +40,8 @@ module Sidekiq
28
40
  1100, 1700, 2500, 3800, 5750,
29
41
  8500, 13000, 20000, 30000, 45000,
30
42
  65000, 100000, 150000, 225000, 335000,
31
- Float::INFINITY # the "maybe your job is too long" bucket
32
- ]
43
+ 1e20 # the "maybe your job is too long" bucket
44
+ ].freeze
33
45
  LABELS = [
34
46
  "20ms", "30ms", "45ms", "65ms", "100ms",
35
47
  "150ms", "225ms", "335ms", "500ms", "750ms",
@@ -37,8 +49,7 @@ module Sidekiq
37
49
  "8.5s", "13s", "20s", "30s", "45s",
38
50
  "65s", "100s", "150s", "225s", "335s",
39
51
  "Slow"
40
- ]
41
-
52
+ ].freeze
42
53
  FETCH = "GET u16 #0 GET u16 #1 GET u16 #2 GET u16 #3 \
43
54
  GET u16 #4 GET u16 #5 GET u16 #6 GET u16 #7 \
44
55
  GET u16 #8 GET u16 #9 GET u16 #10 GET u16 #11 \
@@ -46,6 +57,7 @@ module Sidekiq
46
57
  GET u16 #16 GET u16 #17 GET u16 #18 GET u16 #19 \
47
58
  GET u16 #20 GET u16 #21 GET u16 #22 GET u16 #23 \
48
59
  GET u16 #24 GET u16 #25".split
60
+ HISTOGRAM_TTL = 8 * 60 * 60
49
61
 
50
62
  def each
51
63
  buckets.each { |counter| yield counter.value }
@@ -72,7 +84,7 @@ module Sidekiq
72
84
  def fetch(conn, now = Time.now)
73
85
  window = now.utc.strftime("%d-%H:%-M")
74
86
  key = "#{@klass}-#{window}"
75
- conn.bitfield(key, *FETCH)
87
+ conn.bitfield_ro(key, *FETCH)
76
88
  end
77
89
 
78
90
  def persist(conn, now = Time.now)
@@ -86,7 +98,7 @@ module Sidekiq
86
98
  end
87
99
 
88
100
  conn.bitfield(*cmd) if cmd.size > 3
89
- conn.expire(key, 86400)
101
+ conn.expire(key, HISTOGRAM_TTL)
90
102
  key
91
103
  end
92
104
  end