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,7 +1,7 @@
1
- require "sidekiq"
2
- require "date"
3
- require "set"
1
+ # frozen_string_literal: true
4
2
 
3
+ require "date"
4
+ require "sidekiq"
5
5
  require "sidekiq/metrics/shared"
6
6
 
7
7
  module Sidekiq
@@ -10,94 +10,122 @@ module Sidekiq
10
10
  # Caller sets a set of attributes to act as filters. {#fetch} will call
11
11
  # Redis and return a Hash of results.
12
12
  #
13
- # NB: all metrics and times/dates are UTC only. We specifically do not
13
+ # NB: all metrics and times/dates are UTC only. We explicitly do not
14
14
  # support timezones.
15
15
  class Query
16
- def initialize(pool: Sidekiq.redis_pool, now: Time.now)
16
+ def initialize(pool: nil, now: Time.now)
17
17
  @time = now.utc
18
- @pool = pool
18
+ @pool = pool || Sidekiq.default_configuration.redis_pool
19
19
  @klass = nil
20
20
  end
21
21
 
22
- # Get metric data for all jobs from the last hour
23
- def top_jobs(minutes: 60)
24
- result = Result.new
22
+ ROLLUPS = {
23
+ # minutely aggregates per minute
24
+ minutely: [60, ->(time) { time.strftime("j|%y%m%d|%-H:%M") }],
25
+ # hourly aggregates every 10 minutes so we'll have six data points per hour
26
+ hourly: [600, ->(time) {
27
+ m = time.min
28
+ mins = (m < 10) ? "0" : m.to_s[0]
29
+ time.strftime("j|%y%m%d|%-H:#{mins}")
30
+ }]
31
+ }
25
32
 
33
+ # Get metric data for all jobs from the last hour
34
+ # +class_filter+: return only results for classes matching filter
35
+ # +minutes+: the number of fine-grained minute buckets to retrieve
36
+ # +hours+: the number of coarser-grained 10-minute buckets to retrieve, in hours
37
+ def top_jobs(class_filter: nil, minutes: nil, hours: nil)
26
38
  time = @time
39
+ minutes = 60 unless minutes || hours
40
+
41
+ # DoS protection, sanity check
42
+ minutes = 60 if minutes && minutes > 480
43
+ hours = 72 if hours && hours > 72
44
+
45
+ granularity = hours ? :hourly : :minutely
46
+ result = Result.new(granularity)
47
+ result.ends_at = time
48
+ count = hours ? hours * 6 : minutes
49
+ stride, keyproc = ROLLUPS[granularity]
50
+
27
51
  redis_results = @pool.with do |conn|
28
52
  conn.pipelined do |pipe|
29
- minutes.times do |idx|
30
- key = "j|#{time.strftime("%Y%m%d")}|#{time.hour}:#{time.min}"
53
+ count.times do |idx|
54
+ key = keyproc.call(time)
31
55
  pipe.hgetall key
32
- result.prepend_bucket time
33
- time -= 60
56
+ time -= stride
34
57
  end
35
58
  end
36
59
  end
37
60
 
61
+ result.starts_at = time
38
62
  time = @time
39
63
  redis_results.each do |hash|
40
64
  hash.each do |k, v|
41
65
  kls, metric = k.split("|")
66
+ next if class_filter && !class_filter.match?(kls)
42
67
  result.job_results[kls].add_metric metric, time, v.to_i
43
68
  end
44
- time -= 60
69
+ time -= stride
45
70
  end
46
71
 
47
- result.marks = fetch_marks(result.starts_at..result.ends_at)
48
-
72
+ result.marks = fetch_marks(result.starts_at..result.ends_at, granularity)
49
73
  result
50
74
  end
51
75
 
52
- def for_job(klass, minutes: 60)
53
- result = Result.new
54
-
76
+ def for_job(klass, minutes: nil, hours: nil)
55
77
  time = @time
78
+ minutes = 60 unless minutes || hours
79
+
80
+ # DoS protection, sanity check
81
+ minutes = 60 if minutes && minutes > 480
82
+ hours = 72 if hours && hours > 72
83
+
84
+ granularity = hours ? :hourly : :minutely
85
+ result = Result.new(granularity)
86
+ result.ends_at = time
87
+ count = hours ? hours * 6 : minutes
88
+ stride, keyproc = ROLLUPS[granularity]
89
+
56
90
  redis_results = @pool.with do |conn|
57
91
  conn.pipelined do |pipe|
58
- minutes.times do |idx|
59
- key = "j|#{time.strftime("%Y%m%d")}|#{time.hour}:#{time.min}"
92
+ count.times do |idx|
93
+ key = keyproc.call(time)
60
94
  pipe.hmget key, "#{klass}|ms", "#{klass}|p", "#{klass}|f"
61
- result.prepend_bucket time
62
- time -= 60
95
+ time -= stride
63
96
  end
64
97
  end
65
98
  end
66
99
 
100
+ result.starts_at = time
67
101
  time = @time
68
102
  @pool.with do |conn|
69
103
  redis_results.each do |(ms, p, f)|
70
104
  result.job_results[klass].add_metric "ms", time, ms.to_i if ms
71
105
  result.job_results[klass].add_metric "p", time, p.to_i if p
72
106
  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)
74
- time -= 60
107
+ result.job_results[klass].add_hist time, Histogram.new(klass).fetch(conn, time).reverse if minutes
108
+ time -= stride
75
109
  end
76
110
  end
77
111
 
78
- result.marks = fetch_marks(result.starts_at..result.ends_at)
79
-
112
+ result.marks = fetch_marks(result.starts_at..result.ends_at, granularity)
80
113
  result
81
114
  end
82
115
 
83
- class Result < Struct.new(:starts_at, :ends_at, :size, :buckets, :job_results, :marks)
84
- def initialize
116
+ class Result < Struct.new(:granularity, :starts_at, :ends_at, :size, :job_results, :marks)
117
+ def initialize(granularity = :minutely)
85
118
  super
86
- self.buckets = []
119
+ self.granularity = granularity
87
120
  self.marks = []
88
- self.job_results = Hash.new { |h, k| h[k] = JobResult.new }
89
- end
90
-
91
- def prepend_bucket(time)
92
- buckets.unshift time.strftime("%H:%M")
93
- self.ends_at ||= time
94
- self.starts_at = time
121
+ self.job_results = Hash.new { |h, k| h[k] = JobResult.new(granularity) }
95
122
  end
96
123
  end
97
124
 
98
- class JobResult < Struct.new(:series, :hist, :totals)
99
- def initialize
125
+ class JobResult < Struct.new(:granularity, :series, :hist, :totals)
126
+ def initialize(granularity = :minutely)
100
127
  super
128
+ self.granularity = granularity
101
129
  self.series = Hash.new { |h, k| h[k] = Hash.new(0) }
102
130
  self.hist = Hash.new { |h, k| h[k] = [] }
103
131
  self.totals = Hash.new(0)
@@ -105,18 +133,19 @@ module Sidekiq
105
133
 
106
134
  def add_metric(metric, time, value)
107
135
  totals[metric] += value
108
- series[metric][time.strftime("%H:%M")] += value
136
+ series[metric][Query.bkt_time_s(time, granularity)] += value
109
137
 
110
138
  # Include timing measurements in seconds for convenience
111
139
  add_metric("s", time, value / 1000.0) if metric == "ms"
112
140
  end
113
141
 
114
142
  def add_hist(time, hist_result)
115
- hist[time.strftime("%H:%M")] = hist_result
143
+ hist[Query.bkt_time_s(time, granularity)] = hist_result
116
144
  end
117
145
 
118
146
  def total_avg(metric = "ms")
119
147
  completed = totals["p"] - totals["f"]
148
+ return 0 if completed.zero?
120
149
  totals[metric].to_f / completed
121
150
  end
122
151
 
@@ -128,22 +157,29 @@ module Sidekiq
128
157
  end
129
158
  end
130
159
 
131
- class MarkResult < Struct.new(:time, :label)
132
- def bucket
133
- time.strftime("%H:%M")
134
- end
160
+ MarkResult = Struct.new(:time, :label, :bucket)
161
+
162
+ def self.bkt_time_s(time, granularity)
163
+ # truncate time to ten minutes ("8:40", not "8:43") or one minute
164
+ truncation = (granularity == :hourly) ? 600 : 60
165
+ Time.at(time.to_i - time.to_i % truncation).utc.iso8601
135
166
  end
136
167
 
137
168
  private
138
169
 
139
- def fetch_marks(time_range)
170
+ def fetch_marks(time_range, granularity)
140
171
  [].tap do |result|
141
- marks = @pool.with { |c| c.hgetall("#{@time.strftime("%Y%m%d")}-marks") }
172
+ days = (time_range.begin.utc.to_date..time_range.end.utc.to_date)
173
+ marks = @pool.with { |c|
174
+ c.pipelined { |pipeline|
175
+ days.each { |day| pipeline.hgetall("#{day.strftime("%Y%m%d")}-marks") }
176
+ }
177
+ }.flat_map(&:to_a)
142
178
 
143
179
  marks.each do |timestamp, label|
144
180
  time = Time.parse(timestamp)
145
181
  if time_range.cover? time
146
- result << MarkResult.new(time, label)
182
+ result << MarkResult.new(time, label, Query.bkt_time_s(time, granularity))
147
183
  end
148
184
  end
149
185
  end
@@ -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
@@ -13,7 +25,10 @@ module Sidekiq
13
25
  #
14
26
  # To store this data, we use Redis' BITFIELD command to store unsigned 16-bit counters
15
27
  # per bucket per klass per minute. It's unlikely that most people will be executing more
16
- # than 1000 job/sec for a full minute of a specific type.
28
+ # than 1000 job/sec for a full minute of a specific type (i.e. overflow 65,536).
29
+ #
30
+ # Histograms are only stored at the fine-grained level, they are not rolled up
31
+ # for longer-term buckets.
17
32
  class Histogram
18
33
  include Enumerable
19
34
 
@@ -28,8 +43,8 @@ module Sidekiq
28
43
  1100, 1700, 2500, 3800, 5750,
29
44
  8500, 13000, 20000, 30000, 45000,
30
45
  65000, 100000, 150000, 225000, 335000,
31
- Float::INFINITY # the "maybe your job is too long" bucket
32
- ]
46
+ 1e20 # the "maybe your job is too long" bucket
47
+ ].freeze
33
48
  LABELS = [
34
49
  "20ms", "30ms", "45ms", "65ms", "100ms",
35
50
  "150ms", "225ms", "335ms", "500ms", "750ms",
@@ -37,8 +52,7 @@ module Sidekiq
37
52
  "8.5s", "13s", "20s", "30s", "45s",
38
53
  "65s", "100s", "150s", "225s", "335s",
39
54
  "Slow"
40
- ]
41
-
55
+ ].freeze
42
56
  FETCH = "GET u16 #0 GET u16 #1 GET u16 #2 GET u16 #3 \
43
57
  GET u16 #4 GET u16 #5 GET u16 #6 GET u16 #7 \
44
58
  GET u16 #8 GET u16 #9 GET u16 #10 GET u16 #11 \
@@ -46,6 +60,7 @@ module Sidekiq
46
60
  GET u16 #16 GET u16 #17 GET u16 #18 GET u16 #19 \
47
61
  GET u16 #20 GET u16 #21 GET u16 #22 GET u16 #23 \
48
62
  GET u16 #24 GET u16 #25".split
63
+ HISTOGRAM_TTL = 8 * 60 * 60
49
64
 
50
65
  def each
51
66
  buckets.each { |counter| yield counter.value }
@@ -70,15 +85,15 @@ module Sidekiq
70
85
  end
71
86
 
72
87
  def fetch(conn, now = Time.now)
73
- window = now.utc.strftime("%d-%H:%-M")
74
- key = "#{@klass}-#{window}"
75
- conn.bitfield(key, *FETCH)
88
+ window = now.utc.strftime("%-d-%-H:%-M")
89
+ key = "h|#{@klass}-#{window}"
90
+ conn.bitfield_ro(key, *FETCH)
76
91
  end
77
92
 
78
93
  def persist(conn, now = Time.now)
79
94
  buckets, @buckets = @buckets, []
80
- window = now.utc.strftime("%d-%H:%-M")
81
- key = "#{@klass}-#{window}"
95
+ window = now.utc.strftime("%-d-%-H:%-M")
96
+ key = "h|#{@klass}-#{window}"
82
97
  cmd = [key, "OVERFLOW", "SAT"]
83
98
  buckets.each_with_index do |counter, idx|
84
99
  val = counter.value
@@ -86,7 +101,7 @@ module Sidekiq
86
101
  end
87
102
 
88
103
  conn.bitfield(*cmd) if cmd.size > 3
89
- conn.expire(key, 86400)
104
+ conn.expire(key, HISTOGRAM_TTL)
90
105
  key
91
106
  end
92
107
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "time"
2
4
  require "sidekiq"
3
5
  require "sidekiq/metrics/shared"
@@ -17,23 +19,23 @@ module Sidekiq
17
19
  end
18
20
 
19
21
  def track(queue, klass)
20
- start = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC, :millisecond)
22
+ start = mono_ms
21
23
  time_ms = 0
22
24
  begin
23
25
  begin
24
26
  yield
25
27
  ensure
26
- finish = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC, :millisecond)
28
+ finish = mono_ms
27
29
  time_ms = finish - start
28
30
  end
29
31
  # We don't track time for failed jobs as they can have very unpredictable
30
32
  # execution times. more important to know average time for successful jobs so we
31
33
  # can better recognize when a perf regression is introduced.
32
- @lock.synchronize {
33
- @grams[klass].record_time(time_ms)
34
- @jobs["#{klass}|ms"] += time_ms
35
- @totals["ms"] += time_ms
36
- }
34
+ track_time(klass, time_ms)
35
+ rescue JobRetry::Skip
36
+ # This is raised when iterable job is interrupted.
37
+ track_time(klass, time_ms)
38
+ raise
37
39
  rescue Exception
38
40
  @lock.synchronize {
39
41
  @jobs["#{klass}|f"] += 1
@@ -48,8 +50,8 @@ module Sidekiq
48
50
  end
49
51
  end
50
52
 
51
- LONG_TERM = 90 * 24 * 60 * 60
52
- MID_TERM = 7 * 24 * 60 * 60
53
+ # LONG_TERM = 90 * 24 * 60 * 60
54
+ MID_TERM = 3 * 24 * 60 * 60
53
55
  SHORT_TERM = 8 * 60 * 60
54
56
 
55
57
  def flush(time = Time.now)
@@ -59,12 +61,15 @@ module Sidekiq
59
61
  return if procd == 0 && fails == 0
60
62
 
61
63
  now = time.utc
62
- nowdate = now.strftime("%Y%m%d")
63
- nowhour = now.strftime("%Y%m%d|%-H")
64
- nowmin = now.strftime("%Y%m%d|%-H:%-M")
64
+ # nowdate = now.strftime("%Y%m%d")
65
+ # "250214|8:4" is the 10 minute bucket for Feb 14 2025, 08:43
66
+ nowmid = now.strftime("%y%m%d|%-H:%M")[0..-2]
67
+ # "250214|8:43" is the 1 minute bucket for Feb 14 2025, 08:43
68
+ nowshort = now.strftime("%y%m%d|%-H:%M")
65
69
  count = 0
66
70
 
67
71
  redis do |conn|
72
+ # persist fine-grained histogram data
68
73
  if grams.size > 0
69
74
  conn.pipelined do |pipe|
70
75
  grams.each do |_, gram|
@@ -73,15 +78,16 @@ module Sidekiq
73
78
  end
74
79
  end
75
80
 
81
+ # persist coarse grained execution count + execution millis.
82
+ # note as of today we don't use or do anything with the
83
+ # daily or hourly rollups.
76
84
  [
77
- ["j", jobs, nowdate, LONG_TERM],
78
- ["j", jobs, nowhour, MID_TERM],
79
- ["j", jobs, nowmin, SHORT_TERM]
85
+ # ["j", jobs, nowdate, LONG_TERM],
86
+ ["j", jobs, nowmid, MID_TERM],
87
+ ["j", jobs, nowshort, SHORT_TERM]
80
88
  ].each do |prefix, data, bucket, ttl|
81
- # Quietly seed the new 7.0 stats format so migration is painless.
82
89
  conn.pipelined do |xa|
83
90
  stats = "#{prefix}|#{bucket}"
84
- # logger.debug "Flushing metrics #{stats}"
85
91
  data.each_pair do |key, value|
86
92
  xa.hincrby stats, key, value
87
93
  count += 1
@@ -89,22 +95,34 @@ module Sidekiq
89
95
  xa.expire(stats, ttl)
90
96
  end
91
97
  end
92
- logger.info "Flushed #{count} metrics"
98
+ logger.debug "Flushed #{count} metrics"
93
99
  count
94
100
  end
95
101
  end
96
102
 
97
103
  private
98
104
 
105
+ def track_time(klass, time_ms)
106
+ @lock.synchronize {
107
+ @grams[klass].record_time(time_ms)
108
+ @jobs["#{klass}|ms"] += time_ms
109
+ @totals["ms"] += time_ms
110
+ }
111
+ end
112
+
99
113
  def reset
100
114
  @lock.synchronize {
101
115
  array = [@totals, @jobs, @grams]
102
- @totals = Hash.new(0)
103
- @jobs = Hash.new(0)
104
- @grams = Hash.new { |hash, key| hash[key] = Histogram.new(key) }
116
+ reset_instance_variables
105
117
  array
106
118
  }
107
119
  end
120
+
121
+ def reset_instance_variables
122
+ @totals = Hash.new(0)
123
+ @jobs = Hash.new(0)
124
+ @grams = Hash.new { |hash, key| hash[key] = Histogram.new(key) }
125
+ end
108
126
  end
109
127
 
110
128
  class Middleware
@@ -121,14 +139,15 @@ module Sidekiq
121
139
  end
122
140
  end
123
141
 
124
- if ENV["SIDEKIQ_METRICS_BETA"] == "1"
125
- Sidekiq.configure_server do |config|
126
- exec = Sidekiq::Metrics::ExecutionTracker.new(config)
127
- config.server_middleware do |chain|
128
- chain.add Sidekiq::Metrics::Middleware, exec
129
- end
130
- config.on(:beat) do
131
- exec.flush
132
- end
142
+ Sidekiq.configure_server do |config|
143
+ exec = Sidekiq::Metrics::ExecutionTracker.new(config)
144
+ config.server_middleware do |chain|
145
+ chain.add Sidekiq::Metrics::Middleware, exec
146
+ end
147
+ config.on(:beat) do
148
+ exec.flush
149
+ end
150
+ config.on(:exit) do
151
+ exec.flush
133
152
  end
134
153
  end
@@ -80,15 +80,6 @@ module Sidekiq
80
80
  class Chain
81
81
  include Enumerable
82
82
 
83
- # A unique instance of the middleware chain is created for
84
- # each job executed in order to be thread-safe.
85
- # @param copy [Sidekiq::Middleware::Chain] New instance of Chain
86
- # @returns nil
87
- def initialize_copy(copy)
88
- copy.instance_variable_set(:@entries, entries.dup)
89
- nil
90
- end
91
-
92
83
  # Iterate through each middleware in the chain
93
84
  def each(&block)
94
85
  entries.each(&block)
@@ -105,6 +96,12 @@ module Sidekiq
105
96
  @entries ||= []
106
97
  end
107
98
 
99
+ def copy_for(capsule)
100
+ chain = Sidekiq::Middleware::Chain.new(capsule)
101
+ chain.instance_variable_set(:@entries, entries.dup)
102
+ chain
103
+ end
104
+
108
105
  # Remove all middleware matching the given Class
109
106
  # @param klass [Class]
110
107
  def remove(klass)
@@ -152,6 +149,7 @@ module Sidekiq
152
149
  def exists?(klass)
153
150
  any? { |entry| entry.klass == klass }
154
151
  end
152
+ alias_method :include?, :exists?
155
153
 
156
154
  # @return [Boolean] if the chain contains no middleware
157
155
  def empty?
@@ -168,23 +166,26 @@ module Sidekiq
168
166
 
169
167
  # Used by Sidekiq to execute the middleware at runtime
170
168
  # @api private
171
- def invoke(*args)
169
+ def invoke(*args, &block)
172
170
  return yield if empty?
173
171
 
174
172
  chain = retrieve
175
- traverse_chain = proc do
176
- if chain.empty?
177
- yield
178
- else
179
- chain.shift.call(*args, &traverse_chain)
173
+ traverse(chain, 0, args, &block)
174
+ end
175
+
176
+ private
177
+
178
+ def traverse(chain, index, args, &block)
179
+ if index >= chain.size
180
+ yield
181
+ else
182
+ chain[index].call(*args) do
183
+ traverse(chain, index + 1, args, &block)
180
184
  end
181
185
  end
182
- traverse_chain.call
183
186
  end
184
187
  end
185
188
 
186
- private
187
-
188
189
  # Represents each link in the middleware chain
189
190
  # @api private
190
191
  class Entry
@@ -1,3 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_job/arguments"
1
4
  require "active_support/current_attributes"
2
5
 
3
6
  module Sidekiq
@@ -7,27 +10,33 @@ module Sidekiq
7
10
  # This can be useful for multi-tenancy, i18n locale, timezone, any implicit
8
11
  # per-request attribute. See +ActiveSupport::CurrentAttributes+.
9
12
  #
13
+ # For multiple current attributes, pass an array of current attributes.
14
+ #
10
15
  # @example
11
16
  #
12
17
  # # in your initializer
13
18
  # require "sidekiq/middleware/current_attributes"
14
19
  # Sidekiq::CurrentAttributes.persist("Myapp::Current")
20
+ # # or multiple current attributes
21
+ # Sidekiq::CurrentAttributes.persist(["Myapp::Current", "Myapp::OtherCurrent"])
15
22
  #
16
23
  module CurrentAttributes
24
+ Serializer = ::ActiveJob::Arguments
25
+
17
26
  class Save
18
27
  include Sidekiq::ClientMiddleware
19
28
 
20
- def initialize(cattr)
21
- @strklass = cattr
29
+ def initialize(cattrs)
30
+ @cattrs = cattrs
22
31
  end
23
32
 
24
33
  def call(_, job, _, _)
25
- attrs = @strklass.constantize.attributes
26
- if attrs.any?
27
- if job.has_key?("cattr")
28
- job["cattr"].merge!(attrs)
29
- else
30
- job["cattr"] = attrs
34
+ @cattrs.each do |(key, strklass)|
35
+ if !job.has_key?(key)
36
+ attrs = strklass.constantize.attributes
37
+ # Retries can push the job N times, we don't
38
+ # want retries to reset cattr. #5692, #5090
39
+ job[key] = Serializer.serialize(attrs) if attrs.any?
31
40
  end
32
41
  end
33
42
  yield
@@ -37,26 +46,74 @@ module Sidekiq
37
46
  class Load
38
47
  include Sidekiq::ServerMiddleware
39
48
 
40
- def initialize(cattr)
41
- @strklass = cattr
49
+ def initialize(cattrs)
50
+ @cattrs = cattrs
42
51
  end
43
52
 
44
- def call(_, job, _, &block)
45
- if job.has_key?("cattr")
46
- @strklass.constantize.set(job["cattr"], &block)
47
- else
48
- yield
53
+ def call(_, job, *, &block)
54
+ klass_attrs = {}
55
+
56
+ @cattrs.each do |(key, strklass)|
57
+ next unless job.has_key?(key)
58
+
59
+ klass_attrs[strklass.constantize] = Serializer.deserialize(job[key]).to_h
60
+ end
61
+
62
+ wrap(klass_attrs.to_a, &block)
63
+ end
64
+
65
+ private
66
+
67
+ def wrap(klass_attrs, &block)
68
+ klass, attrs = klass_attrs.shift
69
+ return block.call unless klass
70
+
71
+ retried = false
72
+
73
+ begin
74
+ set_succeeded = false
75
+ klass.set(attrs) do
76
+ set_succeeded = true
77
+ wrap(klass_attrs, &block)
78
+ end
79
+ rescue NoMethodError
80
+ # Don't retry if the no method error didn't come from current attributes
81
+ raise if retried || set_succeeded
82
+
83
+ # It is possible that the `CurrentAttributes` definition
84
+ # was changed before the job started processing.
85
+ attrs = attrs.select { |attr| klass.respond_to?(attr) }
86
+ retried = true
87
+ retry
49
88
  end
50
89
  end
51
90
  end
52
91
 
53
- def self.persist(klass)
54
- Sidekiq.configure_client do |config|
55
- config.client_middleware.add Save, klass.to_s
92
+ class << self
93
+ def persist(klass_or_array, config = Sidekiq.default_configuration)
94
+ cattrs = build_cattrs_hash(klass_or_array)
95
+
96
+ config.client_middleware.prepend Load, cattrs
97
+ config.client_middleware.add Save, cattrs
98
+ config.server_middleware.prepend Load, cattrs
56
99
  end
57
- Sidekiq.configure_server do |config|
58
- config.client_middleware.add Save, klass.to_s
59
- config.server_middleware.add Load, klass.to_s
100
+
101
+ private
102
+
103
+ def build_cattrs_hash(klass_or_array)
104
+ if klass_or_array.is_a?(Array)
105
+ {}.tap do |hash|
106
+ klass_or_array.each_with_index do |klass, index|
107
+ hash[key_at(index)] = klass.to_s
108
+ end
109
+ end
110
+ else
111
+ {key_at(0) => klass_or_array.to_s}
112
+ end
113
+ end
114
+
115
+ def key_at(index)
116
+ (index == 0) ? "cattr" : "cattr_#{index}"
60
117
  end
61
118
  end
62
119
  end