sidekiq 5.2.7 → 8.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (169) hide show
  1. checksums.yaml +4 -4
  2. data/Changes.md +845 -8
  3. data/LICENSE.txt +9 -0
  4. data/README.md +54 -54
  5. data/bin/multi_queue_bench +271 -0
  6. data/bin/sidekiq +22 -3
  7. data/bin/sidekiqload +219 -112
  8. data/bin/sidekiqmon +11 -0
  9. data/bin/webload +69 -0
  10. data/lib/active_job/queue_adapters/sidekiq_adapter.rb +120 -0
  11. data/lib/generators/sidekiq/job_generator.rb +59 -0
  12. data/lib/generators/sidekiq/templates/{worker.rb.erb → job.rb.erb} +2 -2
  13. data/lib/generators/sidekiq/templates/{worker_spec.rb.erb → job_spec.rb.erb} +1 -1
  14. data/lib/generators/sidekiq/templates/{worker_test.rb.erb → job_test.rb.erb} +1 -1
  15. data/lib/sidekiq/api.rb +757 -373
  16. data/lib/sidekiq/capsule.rb +132 -0
  17. data/lib/sidekiq/cli.rb +210 -233
  18. data/lib/sidekiq/client.rb +145 -103
  19. data/lib/sidekiq/component.rb +128 -0
  20. data/lib/sidekiq/config.rb +315 -0
  21. data/lib/sidekiq/deploy.rb +64 -0
  22. data/lib/sidekiq/embedded.rb +64 -0
  23. data/lib/sidekiq/fetch.rb +49 -42
  24. data/lib/sidekiq/iterable_job.rb +56 -0
  25. data/lib/sidekiq/job/interrupt_handler.rb +24 -0
  26. data/lib/sidekiq/job/iterable/active_record_enumerator.rb +53 -0
  27. data/lib/sidekiq/job/iterable/csv_enumerator.rb +47 -0
  28. data/lib/sidekiq/job/iterable/enumerators.rb +135 -0
  29. data/lib/sidekiq/job/iterable.rb +306 -0
  30. data/lib/sidekiq/job.rb +385 -0
  31. data/lib/sidekiq/job_logger.rb +34 -7
  32. data/lib/sidekiq/job_retry.rb +164 -109
  33. data/lib/sidekiq/job_util.rb +113 -0
  34. data/lib/sidekiq/launcher.rb +208 -107
  35. data/lib/sidekiq/logger.rb +80 -0
  36. data/lib/sidekiq/manager.rb +42 -46
  37. data/lib/sidekiq/metrics/query.rb +184 -0
  38. data/lib/sidekiq/metrics/shared.rb +109 -0
  39. data/lib/sidekiq/metrics/tracking.rb +150 -0
  40. data/lib/sidekiq/middleware/chain.rb +113 -56
  41. data/lib/sidekiq/middleware/current_attributes.rb +119 -0
  42. data/lib/sidekiq/middleware/i18n.rb +7 -7
  43. data/lib/sidekiq/middleware/modules.rb +23 -0
  44. data/lib/sidekiq/monitor.rb +147 -0
  45. data/lib/sidekiq/paginator.rb +41 -16
  46. data/lib/sidekiq/processor.rb +146 -127
  47. data/lib/sidekiq/profiler.rb +72 -0
  48. data/lib/sidekiq/rails.rb +46 -43
  49. data/lib/sidekiq/redis_client_adapter.rb +113 -0
  50. data/lib/sidekiq/redis_connection.rb +79 -108
  51. data/lib/sidekiq/ring_buffer.rb +31 -0
  52. data/lib/sidekiq/scheduled.rb +112 -50
  53. data/lib/sidekiq/sd_notify.rb +149 -0
  54. data/lib/sidekiq/systemd.rb +26 -0
  55. data/lib/sidekiq/testing/inline.rb +6 -5
  56. data/lib/sidekiq/testing.rb +91 -90
  57. data/lib/sidekiq/transaction_aware_client.rb +51 -0
  58. data/lib/sidekiq/version.rb +7 -1
  59. data/lib/sidekiq/web/action.rb +125 -60
  60. data/lib/sidekiq/web/application.rb +363 -259
  61. data/lib/sidekiq/web/config.rb +120 -0
  62. data/lib/sidekiq/web/csrf_protection.rb +183 -0
  63. data/lib/sidekiq/web/helpers.rb +241 -120
  64. data/lib/sidekiq/web/router.rb +62 -71
  65. data/lib/sidekiq/web.rb +69 -161
  66. data/lib/sidekiq/worker_compatibility_alias.rb +13 -0
  67. data/lib/sidekiq.rb +94 -182
  68. data/sidekiq.gemspec +26 -16
  69. data/web/assets/images/apple-touch-icon.png +0 -0
  70. data/web/assets/javascripts/application.js +150 -61
  71. data/web/assets/javascripts/base-charts.js +120 -0
  72. data/web/assets/javascripts/chart.min.js +13 -0
  73. data/web/assets/javascripts/chartjs-adapter-date-fns.min.js +7 -0
  74. data/web/assets/javascripts/chartjs-plugin-annotation.min.js +7 -0
  75. data/web/assets/javascripts/dashboard-charts.js +194 -0
  76. data/web/assets/javascripts/dashboard.js +41 -293
  77. data/web/assets/javascripts/metrics.js +280 -0
  78. data/web/assets/stylesheets/style.css +766 -0
  79. data/web/locales/ar.yml +72 -65
  80. data/web/locales/cs.yml +63 -62
  81. data/web/locales/da.yml +61 -53
  82. data/web/locales/de.yml +66 -53
  83. data/web/locales/el.yml +44 -24
  84. data/web/locales/en.yml +94 -66
  85. data/web/locales/es.yml +92 -54
  86. data/web/locales/fa.yml +66 -65
  87. data/web/locales/fr.yml +83 -62
  88. data/web/locales/gd.yml +99 -0
  89. data/web/locales/he.yml +66 -64
  90. data/web/locales/hi.yml +60 -59
  91. data/web/locales/it.yml +93 -54
  92. data/web/locales/ja.yml +75 -64
  93. data/web/locales/ko.yml +53 -52
  94. data/web/locales/lt.yml +84 -0
  95. data/web/locales/nb.yml +62 -61
  96. data/web/locales/nl.yml +53 -52
  97. data/web/locales/pl.yml +46 -45
  98. data/web/locales/{pt-br.yml → pt-BR.yml} +84 -56
  99. data/web/locales/pt.yml +52 -51
  100. data/web/locales/ru.yml +69 -63
  101. data/web/locales/sv.yml +54 -53
  102. data/web/locales/ta.yml +61 -60
  103. data/web/locales/tr.yml +101 -0
  104. data/web/locales/uk.yml +86 -61
  105. data/web/locales/ur.yml +65 -64
  106. data/web/locales/vi.yml +84 -0
  107. data/web/locales/zh-CN.yml +106 -0
  108. data/web/locales/{zh-tw.yml → zh-TW.yml} +43 -9
  109. data/web/views/_footer.erb +31 -19
  110. data/web/views/_job_info.erb +94 -75
  111. data/web/views/_metrics_period_select.erb +15 -0
  112. data/web/views/_nav.erb +14 -21
  113. data/web/views/_paging.erb +23 -19
  114. data/web/views/_poll_link.erb +3 -6
  115. data/web/views/_summary.erb +23 -23
  116. data/web/views/busy.erb +139 -87
  117. data/web/views/dashboard.erb +82 -53
  118. data/web/views/dead.erb +31 -27
  119. data/web/views/filtering.erb +6 -0
  120. data/web/views/layout.erb +15 -29
  121. data/web/views/metrics.erb +84 -0
  122. data/web/views/metrics_for_job.erb +58 -0
  123. data/web/views/morgue.erb +60 -70
  124. data/web/views/profiles.erb +43 -0
  125. data/web/views/queue.erb +50 -39
  126. data/web/views/queues.erb +45 -29
  127. data/web/views/retries.erb +65 -75
  128. data/web/views/retry.erb +32 -27
  129. data/web/views/scheduled.erb +58 -52
  130. data/web/views/scheduled_job_info.erb +1 -1
  131. metadata +96 -76
  132. data/.circleci/config.yml +0 -61
  133. data/.github/contributing.md +0 -32
  134. data/.github/issue_template.md +0 -11
  135. data/.gitignore +0 -15
  136. data/.travis.yml +0 -11
  137. data/3.0-Upgrade.md +0 -70
  138. data/4.0-Upgrade.md +0 -53
  139. data/5.0-Upgrade.md +0 -56
  140. data/COMM-LICENSE +0 -97
  141. data/Ent-Changes.md +0 -238
  142. data/Gemfile +0 -23
  143. data/LICENSE +0 -9
  144. data/Pro-2.0-Upgrade.md +0 -138
  145. data/Pro-3.0-Upgrade.md +0 -44
  146. data/Pro-4.0-Upgrade.md +0 -35
  147. data/Pro-Changes.md +0 -759
  148. data/Rakefile +0 -9
  149. data/bin/sidekiqctl +0 -20
  150. data/code_of_conduct.md +0 -50
  151. data/lib/generators/sidekiq/worker_generator.rb +0 -49
  152. data/lib/sidekiq/core_ext.rb +0 -1
  153. data/lib/sidekiq/ctl.rb +0 -221
  154. data/lib/sidekiq/delay.rb +0 -42
  155. data/lib/sidekiq/exception_handler.rb +0 -29
  156. data/lib/sidekiq/extensions/action_mailer.rb +0 -57
  157. data/lib/sidekiq/extensions/active_record.rb +0 -40
  158. data/lib/sidekiq/extensions/class_methods.rb +0 -40
  159. data/lib/sidekiq/extensions/generic_proxy.rb +0 -31
  160. data/lib/sidekiq/logging.rb +0 -122
  161. data/lib/sidekiq/middleware/server/active_record.rb +0 -23
  162. data/lib/sidekiq/util.rb +0 -66
  163. data/lib/sidekiq/worker.rb +0 -220
  164. data/web/assets/stylesheets/application-rtl.css +0 -246
  165. data/web/assets/stylesheets/application.css +0 -1144
  166. data/web/assets/stylesheets/bootstrap-rtl.min.css +0 -9
  167. data/web/assets/stylesheets/bootstrap.css +0 -5
  168. data/web/locales/zh-cn.yml +0 -68
  169. data/web/views/_status.erb +0 -4
@@ -0,0 +1,184 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "date"
4
+ require "sidekiq"
5
+ require "sidekiq/metrics/shared"
6
+
7
+ module Sidekiq
8
+ module Metrics
9
+ # Allows caller to query for Sidekiq execution metrics within Redis.
10
+ # Caller sets a set of attributes to act as filters. {#fetch} will call
11
+ # Redis and return a Hash of results.
12
+ #
13
+ # NB: all metrics and times/dates are UTC only. We explicitly do not
14
+ # support timezones.
15
+ class Query
16
+ def initialize(pool: nil, now: Time.now)
17
+ @time = now.utc
18
+ @pool = pool || Sidekiq.default_configuration.redis_pool
19
+ @klass = nil
20
+ end
21
+
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
+ }
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)
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
+
51
+ redis_results = @pool.with do |conn|
52
+ conn.pipelined do |pipe|
53
+ count.times do |idx|
54
+ key = keyproc.call(time)
55
+ pipe.hgetall key
56
+ time -= stride
57
+ end
58
+ end
59
+ end
60
+
61
+ result.starts_at = time
62
+ time = @time
63
+ redis_results.each do |hash|
64
+ hash.each do |k, v|
65
+ kls, metric = k.split("|")
66
+ next if class_filter && !class_filter.match?(kls)
67
+ result.job_results[kls].add_metric metric, time, v.to_i
68
+ end
69
+ time -= stride
70
+ end
71
+
72
+ result.marks = fetch_marks(result.starts_at..result.ends_at, granularity)
73
+ result
74
+ end
75
+
76
+ def for_job(klass, minutes: nil, hours: nil)
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
+
90
+ redis_results = @pool.with do |conn|
91
+ conn.pipelined do |pipe|
92
+ count.times do |idx|
93
+ key = keyproc.call(time)
94
+ pipe.hmget key, "#{klass}|ms", "#{klass}|p", "#{klass}|f"
95
+ time -= stride
96
+ end
97
+ end
98
+ end
99
+
100
+ result.starts_at = time
101
+ time = @time
102
+ @pool.with do |conn|
103
+ redis_results.each do |(ms, p, f)|
104
+ result.job_results[klass].add_metric "ms", time, ms.to_i if ms
105
+ result.job_results[klass].add_metric "p", time, p.to_i if p
106
+ result.job_results[klass].add_metric "f", time, f.to_i if f
107
+ result.job_results[klass].add_hist time, Histogram.new(klass).fetch(conn, time).reverse if minutes
108
+ time -= stride
109
+ end
110
+ end
111
+
112
+ result.marks = fetch_marks(result.starts_at..result.ends_at, granularity)
113
+ result
114
+ end
115
+
116
+ class Result < Struct.new(:granularity, :starts_at, :ends_at, :size, :job_results, :marks)
117
+ def initialize(granularity = :minutely)
118
+ super
119
+ self.granularity = granularity
120
+ self.marks = []
121
+ self.job_results = Hash.new { |h, k| h[k] = JobResult.new(granularity) }
122
+ end
123
+ end
124
+
125
+ class JobResult < Struct.new(:granularity, :series, :hist, :totals)
126
+ def initialize(granularity = :minutely)
127
+ super
128
+ self.granularity = granularity
129
+ self.series = Hash.new { |h, k| h[k] = Hash.new(0) }
130
+ self.hist = Hash.new { |h, k| h[k] = [] }
131
+ self.totals = Hash.new(0)
132
+ end
133
+
134
+ def add_metric(metric, time, value)
135
+ totals[metric] += value
136
+ series[metric][Query.bkt_time_s(time, granularity)] += value
137
+
138
+ # Include timing measurements in seconds for convenience
139
+ add_metric("s", time, value / 1000.0) if metric == "ms"
140
+ end
141
+
142
+ def add_hist(time, hist_result)
143
+ hist[Query.bkt_time_s(time, granularity)] = hist_result
144
+ end
145
+
146
+ def total_avg(metric = "ms")
147
+ completed = totals["p"] - totals["f"]
148
+ return 0 if completed.zero?
149
+ totals[metric].to_f / completed
150
+ end
151
+
152
+ def series_avg(metric = "ms")
153
+ series[metric].each_with_object(Hash.new(0)) do |(bucket, value), result|
154
+ completed = series.dig("p", bucket) - series.dig("f", bucket)
155
+ result[bucket] = (completed == 0) ? 0 : value.to_f / completed
156
+ end
157
+ end
158
+ end
159
+
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
166
+ end
167
+
168
+ private
169
+
170
+ def fetch_marks(time_range, granularity)
171
+ [].tap do |result|
172
+ marks = @pool.with { |c| c.hgetall("#{@time.strftime("%Y%m%d")}-marks") }
173
+
174
+ marks.each do |timestamp, label|
175
+ time = Time.parse(timestamp)
176
+ if time_range.cover? time
177
+ result << MarkResult.new(time, label, Query.bkt_time_s(time, granularity))
178
+ end
179
+ end
180
+ end
181
+ end
182
+ end
183
+ end
184
+ end
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sidekiq
4
+ module Metrics
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
19
+
20
+ # Implements space-efficient but statistically useful histogram storage.
21
+ # A precise time histogram stores every time. Instead we break times into a set of
22
+ # known buckets and increment counts of the associated time bucket. Even if we call
23
+ # the histogram a million times, we'll still only store 26 buckets.
24
+ # NB: needs to be thread-safe or resiliant to races.
25
+ #
26
+ # To store this data, we use Redis' BITFIELD command to store unsigned 16-bit counters
27
+ # per bucket per klass per minute. It's unlikely that most people will be executing more
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.
32
+ class Histogram
33
+ include Enumerable
34
+
35
+ # This number represents the maximum milliseconds for this bucket.
36
+ # 20 means all job executions up to 20ms, e.g. if a job takes
37
+ # 280ms, it'll increment bucket[7]. Note we can track job executions
38
+ # up to about 5.5 minutes. After that, it's assumed you're probably
39
+ # not too concerned with its performance.
40
+ BUCKET_INTERVALS = [
41
+ 20, 30, 45, 65, 100,
42
+ 150, 225, 335, 500, 750,
43
+ 1100, 1700, 2500, 3800, 5750,
44
+ 8500, 13000, 20000, 30000, 45000,
45
+ 65000, 100000, 150000, 225000, 335000,
46
+ 1e20 # the "maybe your job is too long" bucket
47
+ ].freeze
48
+ LABELS = [
49
+ "20ms", "30ms", "45ms", "65ms", "100ms",
50
+ "150ms", "225ms", "335ms", "500ms", "750ms",
51
+ "1.1s", "1.7s", "2.5s", "3.8s", "5.75s",
52
+ "8.5s", "13s", "20s", "30s", "45s",
53
+ "65s", "100s", "150s", "225s", "335s",
54
+ "Slow"
55
+ ].freeze
56
+ FETCH = "GET u16 #0 GET u16 #1 GET u16 #2 GET u16 #3 \
57
+ GET u16 #4 GET u16 #5 GET u16 #6 GET u16 #7 \
58
+ GET u16 #8 GET u16 #9 GET u16 #10 GET u16 #11 \
59
+ GET u16 #12 GET u16 #13 GET u16 #14 GET u16 #15 \
60
+ GET u16 #16 GET u16 #17 GET u16 #18 GET u16 #19 \
61
+ GET u16 #20 GET u16 #21 GET u16 #22 GET u16 #23 \
62
+ GET u16 #24 GET u16 #25".split
63
+ HISTOGRAM_TTL = 8 * 60 * 60
64
+
65
+ def each
66
+ buckets.each { |counter| yield counter.value }
67
+ end
68
+
69
+ def label(idx)
70
+ LABELS[idx]
71
+ end
72
+
73
+ attr_reader :buckets
74
+ def initialize(klass)
75
+ @klass = klass
76
+ @buckets = Array.new(BUCKET_INTERVALS.size) { Counter.new }
77
+ end
78
+
79
+ def record_time(ms)
80
+ index_to_use = BUCKET_INTERVALS.each_index do |idx|
81
+ break idx if ms < BUCKET_INTERVALS[idx]
82
+ end
83
+
84
+ @buckets[index_to_use].increment
85
+ end
86
+
87
+ def fetch(conn, now = Time.now)
88
+ window = now.utc.strftime("%-d-%-H:%-M")
89
+ key = "h|#{@klass}-#{window}"
90
+ conn.bitfield_ro(key, *FETCH)
91
+ end
92
+
93
+ def persist(conn, now = Time.now)
94
+ buckets, @buckets = @buckets, []
95
+ window = now.utc.strftime("%-d-%-H:%-M")
96
+ key = "h|#{@klass}-#{window}"
97
+ cmd = [key, "OVERFLOW", "SAT"]
98
+ buckets.each_with_index do |counter, idx|
99
+ val = counter.value
100
+ cmd << "INCRBY" << "u16" << "##{idx}" << val.to_s if val > 0
101
+ end
102
+
103
+ conn.bitfield(*cmd) if cmd.size > 3
104
+ conn.expire(key, HISTOGRAM_TTL)
105
+ key
106
+ end
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,150 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "time"
4
+ require "sidekiq"
5
+ require "sidekiq/metrics/shared"
6
+
7
+ # This file contains the components which track execution metrics within Sidekiq.
8
+ module Sidekiq
9
+ module Metrics
10
+ class ExecutionTracker
11
+ include Sidekiq::Component
12
+
13
+ def initialize(config)
14
+ @config = config
15
+ @jobs = Hash.new(0)
16
+ @totals = Hash.new(0)
17
+ @grams = Hash.new { |hash, key| hash[key] = Histogram.new(key) }
18
+ @lock = Mutex.new
19
+ end
20
+
21
+ def track(queue, klass)
22
+ start = mono_ms
23
+ time_ms = 0
24
+ begin
25
+ begin
26
+ yield
27
+ ensure
28
+ finish = mono_ms
29
+ time_ms = finish - start
30
+ end
31
+ # We don't track time for failed jobs as they can have very unpredictable
32
+ # execution times. more important to know average time for successful jobs so we
33
+ # can better recognize when a perf regression is introduced.
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
39
+ rescue Exception
40
+ @lock.synchronize {
41
+ @jobs["#{klass}|f"] += 1
42
+ @totals["f"] += 1
43
+ }
44
+ raise
45
+ ensure
46
+ @lock.synchronize {
47
+ @jobs["#{klass}|p"] += 1
48
+ @totals["p"] += 1
49
+ }
50
+ end
51
+ end
52
+
53
+ # LONG_TERM = 90 * 24 * 60 * 60
54
+ MID_TERM = 3 * 24 * 60 * 60
55
+ SHORT_TERM = 8 * 60 * 60
56
+
57
+ def flush(time = Time.now)
58
+ totals, jobs, grams = reset
59
+ procd = totals["p"]
60
+ fails = totals["f"]
61
+ return if procd == 0 && fails == 0
62
+
63
+ now = time.utc
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")
69
+ count = 0
70
+
71
+ redis do |conn|
72
+ # persist fine-grained histogram data
73
+ if grams.size > 0
74
+ conn.pipelined do |pipe|
75
+ grams.each do |_, gram|
76
+ gram.persist(pipe, now)
77
+ end
78
+ end
79
+ end
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.
84
+ [
85
+ # ["j", jobs, nowdate, LONG_TERM],
86
+ ["j", jobs, nowmid, MID_TERM],
87
+ ["j", jobs, nowshort, SHORT_TERM]
88
+ ].each do |prefix, data, bucket, ttl|
89
+ conn.pipelined do |xa|
90
+ stats = "#{prefix}|#{bucket}"
91
+ data.each_pair do |key, value|
92
+ xa.hincrby stats, key, value
93
+ count += 1
94
+ end
95
+ xa.expire(stats, ttl)
96
+ end
97
+ end
98
+ logger.debug "Flushed #{count} metrics"
99
+ count
100
+ end
101
+ end
102
+
103
+ private
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
+
113
+ def reset
114
+ @lock.synchronize {
115
+ array = [@totals, @jobs, @grams]
116
+ reset_instance_variables
117
+ array
118
+ }
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
126
+ end
127
+
128
+ class Middleware
129
+ include Sidekiq::ServerMiddleware
130
+
131
+ def initialize(options)
132
+ @exec = options
133
+ end
134
+
135
+ def call(_instance, hash, queue, &block)
136
+ @exec.track(queue, hash["wrapped"] || hash["class"], &block)
137
+ end
138
+ end
139
+ end
140
+ end
141
+
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
+ end
@@ -1,117 +1,160 @@
1
1
  # frozen_string_literal: true
2
+
3
+ require "sidekiq/middleware/modules"
4
+
2
5
  module Sidekiq
3
6
  # Middleware is code configured to run before/after
4
- # a message is processed. It is patterned after Rack
7
+ # a job is processed. It is patterned after Rack
5
8
  # middleware. Middleware exists for the client side
6
9
  # (pushing jobs onto the queue) as well as the server
7
10
  # side (when jobs are actually processed).
8
11
  #
12
+ # Callers will register middleware Classes and Sidekiq will
13
+ # create new instances of the middleware for every job. This
14
+ # is important so that instance state is not shared accidentally
15
+ # between job executions.
16
+ #
9
17
  # To add middleware for the client:
10
18
  #
11
- # Sidekiq.configure_client do |config|
12
- # config.client_middleware do |chain|
13
- # chain.add MyClientHook
19
+ # Sidekiq.configure_client do |config|
20
+ # config.client_middleware do |chain|
21
+ # chain.add MyClientHook
22
+ # end
14
23
  # end
15
- # end
16
24
  #
17
25
  # To modify middleware for the server, just call
18
26
  # with another block:
19
27
  #
20
- # Sidekiq.configure_server do |config|
21
- # config.server_middleware do |chain|
22
- # chain.add MyServerHook
23
- # chain.remove ActiveRecord
28
+ # Sidekiq.configure_server do |config|
29
+ # config.server_middleware do |chain|
30
+ # chain.add MyServerHook
31
+ # chain.remove ActiveRecord
32
+ # end
24
33
  # end
25
- # end
26
34
  #
27
35
  # To insert immediately preceding another entry:
28
36
  #
29
- # Sidekiq.configure_client do |config|
30
- # config.client_middleware do |chain|
31
- # chain.insert_before ActiveRecord, MyClientHook
37
+ # Sidekiq.configure_client do |config|
38
+ # config.client_middleware do |chain|
39
+ # chain.insert_before ActiveRecord, MyClientHook
40
+ # end
32
41
  # end
33
- # end
34
42
  #
35
43
  # To insert immediately after another entry:
36
44
  #
37
- # Sidekiq.configure_client do |config|
38
- # config.client_middleware do |chain|
39
- # chain.insert_after ActiveRecord, MyClientHook
45
+ # Sidekiq.configure_client do |config|
46
+ # config.client_middleware do |chain|
47
+ # chain.insert_after ActiveRecord, MyClientHook
48
+ # end
40
49
  # end
41
- # end
42
50
  #
43
51
  # This is an example of a minimal server middleware:
44
52
  #
45
- # class MyServerHook
46
- # def call(worker_instance, msg, queue)
47
- # puts "Before work"
48
- # yield
49
- # puts "After work"
53
+ # class MyServerHook
54
+ # include Sidekiq::ServerMiddleware
55
+ #
56
+ # def call(job_instance, msg, queue)
57
+ # logger.info "Before job"
58
+ # redis {|conn| conn.get("foo") } # do something in Redis
59
+ # yield
60
+ # logger.info "After job"
61
+ # end
50
62
  # end
51
- # end
52
63
  #
53
64
  # This is an example of a minimal client middleware, note
54
65
  # the method must return the result or the job will not push
55
66
  # to Redis:
56
67
  #
57
- # class MyClientHook
58
- # def call(worker_class, msg, queue, redis_pool)
59
- # puts "Before push"
60
- # result = yield
61
- # puts "After push"
62
- # result
68
+ # class MyClientHook
69
+ # include Sidekiq::ClientMiddleware
70
+ #
71
+ # def call(job_class, msg, queue, redis_pool)
72
+ # logger.info "Before push"
73
+ # result = yield
74
+ # logger.info "After push"
75
+ # result
76
+ # end
63
77
  # end
64
- # end
65
78
  #
66
79
  module Middleware
67
80
  class Chain
68
81
  include Enumerable
69
- attr_reader :entries
70
-
71
- def initialize_copy(copy)
72
- copy.instance_variable_set(:@entries, entries.dup)
73
- end
74
82
 
83
+ # Iterate through each middleware in the chain
75
84
  def each(&block)
76
85
  entries.each(&block)
77
86
  end
78
87
 
79
- def initialize
80
- @entries = []
88
+ # @api private
89
+ def initialize(config = nil) # :nodoc:
90
+ @config = config
91
+ @entries = nil
81
92
  yield self if block_given?
82
93
  end
83
94
 
95
+ def entries
96
+ @entries ||= []
97
+ end
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
+
105
+ # Remove all middleware matching the given Class
106
+ # @param klass [Class]
84
107
  def remove(klass)
85
108
  entries.delete_if { |entry| entry.klass == klass }
86
109
  end
87
110
 
111
+ # Add the given middleware to the end of the chain.
112
+ # Sidekiq will call `klass.new(*args)` to create a clean
113
+ # copy of your middleware for every job executed.
114
+ #
115
+ # chain.add(Statsd::Metrics, { collector: "localhost:8125" })
116
+ #
117
+ # @param klass [Class] Your middleware class
118
+ # @param *args [Array<Object>] Set of arguments to pass to every instance of your middleware
88
119
  def add(klass, *args)
89
- remove(klass) if exists?(klass)
90
- entries << Entry.new(klass, *args)
120
+ remove(klass)
121
+ entries << Entry.new(@config, klass, *args)
91
122
  end
92
123
 
124
+ # Identical to {#add} except the middleware is added to the front of the chain.
93
125
  def prepend(klass, *args)
94
- remove(klass) if exists?(klass)
95
- entries.insert(0, Entry.new(klass, *args))
126
+ remove(klass)
127
+ entries.insert(0, Entry.new(@config, klass, *args))
96
128
  end
97
129
 
130
+ # Inserts +newklass+ before +oldklass+ in the chain.
131
+ # Useful if one middleware must run before another middleware.
98
132
  def insert_before(oldklass, newklass, *args)
99
133
  i = entries.index { |entry| entry.klass == newklass }
100
- new_entry = i.nil? ? Entry.new(newklass, *args) : entries.delete_at(i)
134
+ new_entry = i.nil? ? Entry.new(@config, newklass, *args) : entries.delete_at(i)
101
135
  i = entries.index { |entry| entry.klass == oldklass } || 0
102
136
  entries.insert(i, new_entry)
103
137
  end
104
138
 
139
+ # Inserts +newklass+ after +oldklass+ in the chain.
140
+ # Useful if one middleware must run after another middleware.
105
141
  def insert_after(oldklass, newklass, *args)
106
142
  i = entries.index { |entry| entry.klass == newklass }
107
- new_entry = i.nil? ? Entry.new(newklass, *args) : entries.delete_at(i)
143
+ new_entry = i.nil? ? Entry.new(@config, newklass, *args) : entries.delete_at(i)
108
144
  i = entries.index { |entry| entry.klass == oldklass } || entries.count - 1
109
- entries.insert(i+1, new_entry)
145
+ entries.insert(i + 1, new_entry)
110
146
  end
111
147
 
148
+ # @return [Boolean] if the given class is already in the chain
112
149
  def exists?(klass)
113
150
  any? { |entry| entry.klass == klass }
114
151
  end
152
+ alias_method :include?, :exists?
153
+
154
+ # @return [Boolean] if the chain contains no middleware
155
+ def empty?
156
+ @entries.nil? || @entries.empty?
157
+ end
115
158
 
116
159
  def retrieve
117
160
  map(&:make_new)
@@ -121,29 +164,43 @@ module Sidekiq
121
164
  entries.clear
122
165
  end
123
166
 
124
- def invoke(*args)
125
- chain = retrieve.dup
126
- traverse_chain = lambda do
127
- if chain.empty?
128
- yield
129
- else
130
- chain.shift.call(*args, &traverse_chain)
167
+ # Used by Sidekiq to execute the middleware at runtime
168
+ # @api private
169
+ def invoke(*args, &block)
170
+ return yield if empty?
171
+
172
+ chain = retrieve
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)
131
184
  end
132
185
  end
133
- traverse_chain.call
134
186
  end
135
187
  end
136
188
 
189
+ # Represents each link in the middleware chain
190
+ # @api private
137
191
  class Entry
138
192
  attr_reader :klass
139
193
 
140
- def initialize(klass, *args)
194
+ def initialize(config, klass, *args)
195
+ @config = config
141
196
  @klass = klass
142
- @args = args
197
+ @args = args
143
198
  end
144
199
 
145
200
  def make_new
146
- @klass.new(*@args)
201
+ x = @klass.new(*@args)
202
+ x.config = @config if @config && x.respond_to?(:config=)
203
+ x
147
204
  end
148
205
  end
149
206
  end