sidekiq 6.5.10 → 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 (177) hide show
  1. checksums.yaml +4 -4
  2. data/Changes.md +568 -18
  3. data/README.md +66 -38
  4. data/bin/kiq +17 -0
  5. data/bin/lint-herb +13 -0
  6. data/bin/multi_queue_bench +271 -0
  7. data/bin/sidekiq +3 -8
  8. data/bin/sidekiqload +215 -118
  9. data/bin/sidekiqmon +3 -0
  10. data/bin/webload +69 -0
  11. data/lib/active_job/queue_adapters/sidekiq_adapter.rb +124 -0
  12. data/lib/generators/sidekiq/job_generator.rb +17 -3
  13. data/lib/generators/sidekiq/templates/job.rb.erb +1 -1
  14. data/lib/sidekiq/api.rb +526 -218
  15. data/lib/sidekiq/capsule.rb +135 -0
  16. data/lib/sidekiq/cli.rb +88 -96
  17. data/lib/sidekiq/client.rb +114 -47
  18. data/lib/sidekiq/component.rb +71 -4
  19. data/lib/sidekiq/config.rb +316 -0
  20. data/lib/sidekiq/deploy.rb +64 -0
  21. data/lib/sidekiq/embedded.rb +64 -0
  22. data/lib/sidekiq/fetch.rb +12 -14
  23. data/lib/sidekiq/iterable_job.rb +56 -0
  24. data/lib/sidekiq/job/interrupt_handler.rb +24 -0
  25. data/lib/sidekiq/job/iterable/active_record_enumerator.rb +53 -0
  26. data/lib/sidekiq/job/iterable/csv_enumerator.rb +47 -0
  27. data/lib/sidekiq/job/iterable/enumerators.rb +135 -0
  28. data/lib/sidekiq/job/iterable.rb +322 -0
  29. data/lib/sidekiq/job.rb +397 -10
  30. data/lib/sidekiq/job_logger.rb +14 -11
  31. data/lib/sidekiq/job_retry.rb +78 -28
  32. data/lib/sidekiq/job_util.rb +58 -16
  33. data/lib/sidekiq/launcher.rb +86 -70
  34. data/lib/sidekiq/loader.rb +57 -0
  35. data/lib/sidekiq/logger.rb +25 -94
  36. data/lib/sidekiq/manager.rb +11 -13
  37. data/lib/sidekiq/metrics/query.rb +84 -48
  38. data/lib/sidekiq/metrics/shared.rb +29 -14
  39. data/lib/sidekiq/metrics/tracking.rb +49 -30
  40. data/lib/sidekiq/middleware/chain.rb +19 -18
  41. data/lib/sidekiq/middleware/current_attributes.rb +78 -21
  42. data/lib/sidekiq/middleware/i18n.rb +2 -0
  43. data/lib/sidekiq/middleware/modules.rb +2 -0
  44. data/lib/sidekiq/monitor.rb +15 -5
  45. data/lib/sidekiq/paginator.rb +26 -4
  46. data/lib/sidekiq/processor.rb +78 -65
  47. data/lib/sidekiq/profiler.rb +73 -0
  48. data/lib/sidekiq/rails.rb +45 -60
  49. data/lib/sidekiq/redis_client_adapter.rb +35 -72
  50. data/lib/sidekiq/redis_connection.rb +54 -114
  51. data/lib/sidekiq/ring_buffer.rb +3 -0
  52. data/lib/sidekiq/scheduled.rb +28 -30
  53. data/lib/sidekiq/systemd.rb +2 -0
  54. data/lib/sidekiq/test_api.rb +331 -0
  55. data/lib/sidekiq/testing/inline.rb +2 -30
  56. data/lib/sidekiq/testing.rb +2 -342
  57. data/lib/sidekiq/transaction_aware_client.rb +23 -9
  58. data/lib/sidekiq/tui/controls.rb +53 -0
  59. data/lib/sidekiq/tui/filtering.rb +53 -0
  60. data/lib/sidekiq/tui/tabs/base_tab.rb +230 -0
  61. data/lib/sidekiq/tui/tabs/busy.rb +104 -0
  62. data/lib/sidekiq/tui/tabs/dead.rb +14 -0
  63. data/lib/sidekiq/tui/tabs/home.rb +115 -0
  64. data/lib/sidekiq/tui/tabs/metrics.rb +114 -0
  65. data/lib/sidekiq/tui/tabs/queues.rb +83 -0
  66. data/lib/sidekiq/tui/tabs/retries.rb +14 -0
  67. data/lib/sidekiq/tui/tabs/scheduled.rb +14 -0
  68. data/lib/sidekiq/tui/tabs/set_tab.rb +91 -0
  69. data/lib/sidekiq/tui/tabs.rb +15 -0
  70. data/lib/sidekiq/tui.rb +390 -0
  71. data/lib/sidekiq/version.rb +6 -1
  72. data/lib/sidekiq/web/action.rb +149 -64
  73. data/lib/sidekiq/web/application.rb +379 -281
  74. data/lib/sidekiq/web/config.rb +117 -0
  75. data/lib/sidekiq/web/helpers.rb +190 -71
  76. data/lib/sidekiq/web/router.rb +62 -75
  77. data/lib/sidekiq/web.rb +68 -101
  78. data/lib/sidekiq/worker_compatibility_alias.rb +13 -0
  79. data/lib/sidekiq.rb +85 -269
  80. data/sidekiq.gemspec +15 -11
  81. data/web/assets/images/logo.png +0 -0
  82. data/web/assets/images/status.png +0 -0
  83. data/web/assets/javascripts/application.js +84 -24
  84. data/web/assets/javascripts/base-charts.js +120 -0
  85. data/web/assets/javascripts/chartjs-adapter-date-fns.min.js +7 -0
  86. data/web/assets/javascripts/dashboard-charts.js +194 -0
  87. data/web/assets/javascripts/dashboard.js +18 -234
  88. data/web/assets/javascripts/metrics.js +138 -120
  89. data/web/assets/stylesheets/style.css +776 -0
  90. data/web/locales/ar.yml +72 -70
  91. data/web/locales/cs.yml +64 -62
  92. data/web/locales/da.yml +62 -53
  93. data/web/locales/de.yml +67 -65
  94. data/web/locales/el.yml +4 -7
  95. data/web/locales/en.yml +89 -71
  96. data/web/locales/es.yml +91 -68
  97. data/web/locales/fa.yml +67 -65
  98. data/web/locales/fr.yml +82 -67
  99. data/web/locales/gd.yml +110 -0
  100. data/web/locales/he.yml +67 -64
  101. data/web/locales/hi.yml +95 -59
  102. data/web/locales/it.yml +94 -54
  103. data/web/locales/ja.yml +69 -70
  104. data/web/locales/ko.yml +54 -52
  105. data/web/locales/lt.yml +68 -66
  106. data/web/locales/nb.yml +63 -61
  107. data/web/locales/nl.yml +54 -52
  108. data/web/locales/pl.yml +47 -45
  109. data/web/locales/{pt-br.yml → pt-BR.yml} +81 -70
  110. data/web/locales/pt.yml +53 -51
  111. data/web/locales/ru.yml +69 -66
  112. data/web/locales/sv.yml +55 -53
  113. data/web/locales/ta.yml +100 -64
  114. data/web/locales/tr.yml +102 -0
  115. data/web/locales/uk.yml +87 -61
  116. data/web/locales/ur.yml +66 -64
  117. data/web/locales/vi.yml +69 -67
  118. data/web/locales/zh-CN.yml +107 -0
  119. data/web/locales/{zh-tw.yml → zh-TW.yml} +13 -3
  120. data/web/views/_footer.html.erb +32 -0
  121. data/web/views/_job_info.html.erb +115 -0
  122. data/web/views/_metrics_period_select.html.erb +15 -0
  123. data/web/views/_nav.html.erb +45 -0
  124. data/web/views/_paging.html.erb +26 -0
  125. data/web/views/_poll_link.html.erb +4 -0
  126. data/web/views/_summary.html.erb +40 -0
  127. data/web/views/busy.html.erb +155 -0
  128. data/web/views/dashboard.html.erb +104 -0
  129. data/web/views/dead.html.erb +38 -0
  130. data/web/views/filtering.html.erb +6 -0
  131. data/web/views/layout.html.erb +26 -0
  132. data/web/views/metrics.html.erb +85 -0
  133. data/web/views/metrics_for_job.html.erb +58 -0
  134. data/web/views/morgue.html.erb +69 -0
  135. data/web/views/profiles.html.erb +43 -0
  136. data/web/views/queue.html.erb +57 -0
  137. data/web/views/queues.html.erb +46 -0
  138. data/web/views/retries.html.erb +77 -0
  139. data/web/views/retry.html.erb +39 -0
  140. data/web/views/scheduled.html.erb +64 -0
  141. data/web/views/{scheduled_job_info.erb → scheduled_job_info.html.erb} +3 -3
  142. metadata +113 -70
  143. data/lib/sidekiq/delay.rb +0 -43
  144. data/lib/sidekiq/extensions/action_mailer.rb +0 -48
  145. data/lib/sidekiq/extensions/active_record.rb +0 -43
  146. data/lib/sidekiq/extensions/class_methods.rb +0 -43
  147. data/lib/sidekiq/extensions/generic_proxy.rb +0 -33
  148. data/lib/sidekiq/metrics/deploy.rb +0 -47
  149. data/lib/sidekiq/web/csrf_protection.rb +0 -180
  150. data/lib/sidekiq/worker.rb +0 -370
  151. data/web/assets/javascripts/graph.js +0 -16
  152. data/web/assets/stylesheets/application-dark.css +0 -143
  153. data/web/assets/stylesheets/application-rtl.css +0 -242
  154. data/web/assets/stylesheets/application.css +0 -999
  155. data/web/assets/stylesheets/bootstrap-rtl.min.css +0 -9
  156. data/web/assets/stylesheets/bootstrap.css +0 -5
  157. data/web/locales/zh-cn.yml +0 -93
  158. data/web/views/_footer.erb +0 -20
  159. data/web/views/_job_info.erb +0 -89
  160. data/web/views/_nav.erb +0 -52
  161. data/web/views/_paging.erb +0 -23
  162. data/web/views/_poll_link.erb +0 -4
  163. data/web/views/_status.erb +0 -4
  164. data/web/views/_summary.erb +0 -40
  165. data/web/views/busy.erb +0 -137
  166. data/web/views/dashboard.erb +0 -84
  167. data/web/views/dead.erb +0 -34
  168. data/web/views/layout.erb +0 -42
  169. data/web/views/metrics.erb +0 -69
  170. data/web/views/metrics_for_job.erb +0 -87
  171. data/web/views/morgue.erb +0 -78
  172. data/web/views/queue.erb +0 -59
  173. data/web/views/queues.erb +0 -38
  174. data/web/views/retries.erb +0 -83
  175. data/web/views/retry.erb +0 -34
  176. data/web/views/scheduled.erb +0 -57
  177. /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,10 +57,15 @@ 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
52
- item["created_at"] ||= Time.now.to_f
60
+ item["retry_for"] = item["retry_for"].to_i if item["retry_for"]
61
+ item["created_at"] ||= now_in_millis
53
62
  item
54
63
  end
55
64
 
65
+ def now_in_millis
66
+ ::Process.clock_gettime(::Process::CLOCK_REALTIME, :millisecond)
67
+ end
68
+
56
69
  def normalized_hash(item_class)
57
70
  if item_class.is_a?(Class)
58
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)
@@ -64,8 +77,37 @@ module Sidekiq
64
77
 
65
78
  private
66
79
 
67
- def json_safe?(item)
68
- JSON.parse(JSON.dump(item["args"])) == item["args"]
80
+ RECURSIVE_JSON_UNSAFE = {
81
+ Integer => ->(val) {},
82
+ Float => ->(val) {},
83
+ TrueClass => ->(val) {},
84
+ FalseClass => ->(val) {},
85
+ NilClass => ->(val) {},
86
+ String => ->(val) {},
87
+ Array => ->(val) {
88
+ val.each do |e|
89
+ unsafe_item = RECURSIVE_JSON_UNSAFE[e.class].call(e)
90
+ return unsafe_item unless unsafe_item.nil?
91
+ end
92
+ nil
93
+ },
94
+ Hash => ->(val) {
95
+ val.each do |k, v|
96
+ return k unless String === k
97
+
98
+ unsafe_item = RECURSIVE_JSON_UNSAFE[v.class].call(v)
99
+ return unsafe_item unless unsafe_item.nil?
100
+ end
101
+ nil
102
+ }
103
+ }
104
+
105
+ RECURSIVE_JSON_UNSAFE.default = ->(val) { val }
106
+ RECURSIVE_JSON_UNSAFE.compare_by_identity
107
+ private_constant :RECURSIVE_JSON_UNSAFE
108
+
109
+ def json_unsafe?(item)
110
+ RECURSIVE_JSON_UNSAFE[item.class].call(item)
69
111
  end
70
112
  end
71
113
  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,68 +16,89 @@ 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
71
+ fire_event(:exit, reverse: true)
63
72
  end
64
73
 
65
74
  def stopping?
66
75
  @done
67
76
  end
68
77
 
69
- private unless $TESTING
78
+ # If embedding Sidekiq, you can have the process heartbeat
79
+ # call this method to regularly heartbeat rather than creating
80
+ # a separate thread.
81
+ def heartbeat
82
+ ❤
83
+ end
70
84
 
71
- BEAT_PAUSE = 5
85
+ private
86
+
87
+ BEAT_PAUSE = 10
72
88
 
73
89
  def start_heartbeat
74
90
  loop do
75
- heartbeat
91
+ beat
76
92
  sleep BEAT_PAUSE
77
93
  end
78
94
  logger.info("Heartbeat stopping...")
79
95
  end
80
96
 
97
+ def beat
98
+ $0 = PROCTITLES.map { |proc| proc.call(self, to_data) }.compact.join(" ") unless @embedded
99
+ ❤
100
+ end
101
+
81
102
  def clear_heartbeat
82
103
  flush_stats
83
104
 
@@ -94,12 +115,6 @@ module Sidekiq
94
115
  # best effort, ignore network errors
95
116
  end
96
117
 
97
- def heartbeat
98
- $0 = PROCTITLES.map { |proc| proc.call(self, to_data) }.compact.join(" ")
99
-
100
- ❤
101
- end
102
-
103
118
  def flush_stats
104
119
  fails = Processor::FAILURE.reset
105
120
  procd = Processor::PROCESSED.reset
@@ -107,7 +122,7 @@ module Sidekiq
107
122
 
108
123
  nowdate = Time.now.utc.strftime("%Y-%m-%d")
109
124
  begin
110
- Sidekiq.redis do |conn|
125
+ redis do |conn|
111
126
  conn.pipelined do |pipeline|
112
127
  pipeline.incrby("stat:processed", procd)
113
128
  pipeline.incrby("stat:processed:#{nowdate}", procd)
@@ -119,9 +134,7 @@ module Sidekiq
119
134
  end
120
135
  end
121
136
  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}")
137
+ logger.warn("Unable to flush stats: #{ex}")
125
138
  end
126
139
  end
127
140
 
@@ -129,32 +142,27 @@ module Sidekiq
129
142
  key = identity
130
143
  fails = procd = 0
131
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
+
132
151
  begin
133
- fails = Processor::FAILURE.reset
134
- procd = Processor::PROCESSED.reset
135
- curstate = Processor::WORK_STATE.dup
152
+ flush_stats
136
153
 
137
- nowdate = Time.now.utc.strftime("%Y-%m-%d")
154
+ curstate = Processor::WORK_STATE.dup
155
+ curstate.transform_values! { |val| Sidekiq.dump_json(val) }
138
156
 
139
157
  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
158
  # work is the current set of executing jobs
151
159
  work_key = "#{key}:work"
152
- conn.pipelined do |transaction|
160
+ conn.multi do |transaction|
153
161
  transaction.unlink(work_key)
154
- curstate.each_pair do |tid, hash|
155
- transaction.hset(work_key, tid, Sidekiq.dump_json(hash))
162
+ if curstate.size > 0
163
+ transaction.hset(work_key, curstate)
164
+ transaction.expire(work_key, 60)
156
165
  end
157
- transaction.expire(work_key, 60)
158
166
  end
159
167
  end
160
168
 
@@ -163,11 +171,12 @@ module Sidekiq
163
171
  fails = procd = 0
164
172
  kb = memory_usage(::Process.pid)
165
173
 
166
- _, exists, _, _, msg = redis { |conn|
174
+ _, exists, _, _, signal = redis { |conn|
167
175
  conn.multi { |transaction|
168
176
  transaction.sadd("processes", [key])
169
- transaction.exists?(key)
170
- transaction.hmset(key, "info", to_json,
177
+ transaction.exists(key)
178
+ transaction.hset(key, "info", to_json,
179
+ "concurrency", @config.total_concurrency,
171
180
  "busy", curstate.size,
172
181
  "beat", Time.now.to_f,
173
182
  "rtt_us", rtt,
@@ -179,12 +188,10 @@ module Sidekiq
179
188
  }
180
189
 
181
190
  # first heartbeat or recovering from an outage and need to reestablish our heartbeat
182
- fire_event(:heartbeat) unless exists
191
+ fire_event(:heartbeat) unless exists > 0
183
192
  fire_event(:beat, oneshot: false)
184
193
 
185
- return unless msg
186
-
187
- ::Process.kill(msg, ::Process.pid)
194
+ ::Process.kill(signal, ::Process.pid) if signal && !@embedded
188
195
  rescue => e
189
196
  # ignore all redis/network issues
190
197
  logger.error("heartbeat: #{e}")
@@ -214,11 +221,11 @@ module Sidekiq
214
221
  # Log a warning if it's a disaster.
215
222
  if RTT_READINGS.all? { |x| x > RTT_WARNING_LEVEL }
216
223
  logger.warn <<~EOM
217
- Your Redis network connection is performing extremely poorly.
224
+ Your Redis network connection appears to be performing poorly.
218
225
  Last RTT readings were #{RTT_READINGS.buffer.inspect}, ideally these should be < 1000.
219
- Ensure Redis is running in the same AZ or datacenter as Sidekiq.
220
- 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
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
222
229
  EOM
223
230
  RTT_READINGS.reset
224
231
  end
@@ -228,7 +235,7 @@ module Sidekiq
228
235
  MEMORY_GRABBER = case RUBY_PLATFORM
229
236
  when /linux/
230
237
  ->(pid) {
231
- IO.readlines("/proc/#{$$}/status").each do |line|
238
+ IO.readlines("/proc/#{pid}/status").each do |line|
232
239
  next unless line.start_with?("VmRSS:")
233
240
  break line.split[1].to_i
234
241
  end
@@ -251,10 +258,19 @@ module Sidekiq
251
258
  "started_at" => Time.now.to_f,
252
259
  "pid" => ::Process.pid,
253
260
  "tag" => @config[:tag] || "",
254
- "concurrency" => @config[:concurrency],
255
- "queues" => @config[:queues].uniq,
256
- "labels" => @config[:labels],
257
- "identity" => identity
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
267
+ "queues" => @config.capsules.values.flat_map { |cap| cap.queues }.uniq,
268
+ "weights" => @config.capsules.values.map(&:weights),
269
+ #####
270
+ "labels" => @config[:labels].to_a,
271
+ "identity" => identity,
272
+ "version" => Sidekiq::VERSION,
273
+ "embedded" => @embedded
258
274
  }
259
275
  end
260
276
 
@@ -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,117 +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
- Sidekiq.logger.warn("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
-
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
- end
94
-
95
25
  class Logger < ::Logger
96
- include LoggingUtils
97
-
98
- def initialize(*args, **kwargs)
99
- super
100
- self.formatter = Sidekiq.log_formatter
101
- end
102
-
103
26
  module Formatters
104
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
+
105
36
  def tid
106
37
  Thread.current["sidekiq_tid"] ||= (Thread.current.object_id ^ ::Process.pid).to_s(36)
107
38
  end
108
39
 
109
- def ctx
110
- 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(" ")}"
111
49
  end
50
+ end
112
51
 
113
- def format_context
114
- if ctx.any?
115
- " " + ctx.compact.map { |k, v|
116
- case v
117
- when Array
118
- "#{k}=#{v.join(",")}"
119
- else
120
- "#{k}=#{v}"
121
- end
122
- }.join(" ")
123
- 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"
124
55
  end
125
56
  end
126
57
 
127
- class Pretty < Base
58
+ class Plain < Base
128
59
  def call(severity, time, program_name, message)
129
- "#{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"
130
61
  end
131
62
  end
132
63
 
133
64
  class WithoutTimestamp < Pretty
134
65
  def call(severity, time, program_name, message)
135
- "pid=#{::Process.pid} tid=#{tid}#{format_context} #{severity}: #{message}\n"
66
+ "#{COLORS[severity]} pid=#{::Process.pid} tid=#{tid}#{format_context}: #{message}\n"
136
67
  end
137
68
  end
138
69
 
@@ -145,7 +76,7 @@ module Sidekiq
145
76
  lvl: severity,
146
77
  msg: message
147
78
  }
148
- c = ctx
79
+ c = Sidekiq::Context.current
149
80
  hash["ctx"] = c unless c.empty?
150
81
 
151
82
  Sidekiq.dump_json(hash) << "\n"
@@ -1,8 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "sidekiq/processor"
4
- require "sidekiq/fetch"
5
- require "set"
6
4
 
7
5
  module Sidekiq
8
6
  ##
@@ -23,19 +21,19 @@ module Sidekiq
23
21
  include Sidekiq::Component
24
22
 
25
23
  attr_reader :workers
24
+ attr_reader :capsule
26
25
 
27
- def initialize(options = {})
28
- @config = options
29
- logger.debug { options.inspect }
30
- @count = options[:concurrency] || 10
26
+ def initialize(capsule)
27
+ @config = @capsule = capsule
28
+ @count = capsule.concurrency
31
29
  raise ArgumentError, "Concurrency of #{@count} is not supported" if @count < 1
32
30
 
33
31
  @done = false
34
32
  @workers = Set.new
33
+ @plock = Mutex.new
35
34
  @count.times do
36
35
  @workers << Processor.new(@config, &method(:processor_result))
37
36
  end
38
- @plock = Mutex.new
39
37
  end
40
38
 
41
39
  def start
@@ -46,14 +44,12 @@ module Sidekiq
46
44
  return if @done
47
45
  @done = true
48
46
 
49
- logger.info { "Terminating quiet threads" }
47
+ logger.info { "Terminating quiet threads for #{capsule.name} capsule" }
50
48
  @workers.each(&:terminate)
51
- fire_event(:quiet, reverse: true)
52
49
  end
53
50
 
54
51
  def stop(deadline)
55
52
  quiet
56
- fire_event(:shutdown, reverse: true)
57
53
 
58
54
  # some of the shutdown events can be async,
59
55
  # we don't have any way to know when they're done but
@@ -66,17 +62,20 @@ module Sidekiq
66
62
  return if @workers.empty?
67
63
 
68
64
  hard_shutdown
65
+ ensure
66
+ capsule.stop
69
67
  end
70
68
 
71
69
  def processor_result(processor, reason = nil)
72
70
  @plock.synchronize do
73
71
  @workers.delete(processor)
74
- unless @done
72
+ if !@done && @count > @workers.size
75
73
  p = Processor.new(@config, &method(:processor_result))
76
74
  @workers << p
77
75
  p.start
78
76
  end
79
77
  end
78
+ nil
80
79
  end
81
80
 
82
81
  def stopped?
@@ -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|