sidekiq 6.5.1 → 7.3.6

Sign up to get free protection for your applications and to get access to all the features.
Files changed (125) hide show
  1. checksums.yaml +4 -4
  2. data/Changes.md +376 -12
  3. data/README.md +43 -35
  4. data/bin/multi_queue_bench +271 -0
  5. data/bin/sidekiq +3 -8
  6. data/bin/sidekiqload +213 -118
  7. data/bin/sidekiqmon +3 -0
  8. data/lib/active_job/queue_adapters/sidekiq_adapter.rb +88 -0
  9. data/lib/generators/sidekiq/job_generator.rb +2 -0
  10. data/lib/sidekiq/api.rb +378 -173
  11. data/lib/sidekiq/capsule.rb +132 -0
  12. data/lib/sidekiq/cli.rb +61 -63
  13. data/lib/sidekiq/client.rb +89 -40
  14. data/lib/sidekiq/component.rb +6 -2
  15. data/lib/sidekiq/config.rb +305 -0
  16. data/lib/sidekiq/deploy.rb +64 -0
  17. data/lib/sidekiq/embedded.rb +63 -0
  18. data/lib/sidekiq/fetch.rb +11 -14
  19. data/lib/sidekiq/iterable_job.rb +55 -0
  20. data/lib/sidekiq/job/interrupt_handler.rb +24 -0
  21. data/lib/sidekiq/job/iterable/active_record_enumerator.rb +53 -0
  22. data/lib/sidekiq/job/iterable/csv_enumerator.rb +47 -0
  23. data/lib/sidekiq/job/iterable/enumerators.rb +135 -0
  24. data/lib/sidekiq/job/iterable.rb +294 -0
  25. data/lib/sidekiq/job.rb +382 -10
  26. data/lib/sidekiq/job_logger.rb +8 -7
  27. data/lib/sidekiq/job_retry.rb +89 -46
  28. data/lib/sidekiq/job_util.rb +53 -15
  29. data/lib/sidekiq/launcher.rb +77 -69
  30. data/lib/sidekiq/logger.rb +2 -27
  31. data/lib/sidekiq/manager.rb +9 -11
  32. data/lib/sidekiq/metrics/query.rb +158 -0
  33. data/lib/sidekiq/metrics/shared.rb +106 -0
  34. data/lib/sidekiq/metrics/tracking.rb +148 -0
  35. data/lib/sidekiq/middleware/chain.rb +84 -48
  36. data/lib/sidekiq/middleware/current_attributes.rb +87 -20
  37. data/lib/sidekiq/middleware/modules.rb +2 -0
  38. data/lib/sidekiq/monitor.rb +19 -5
  39. data/lib/sidekiq/paginator.rb +11 -3
  40. data/lib/sidekiq/processor.rb +67 -56
  41. data/lib/sidekiq/rails.rb +22 -16
  42. data/lib/sidekiq/redis_client_adapter.rb +31 -71
  43. data/lib/sidekiq/redis_connection.rb +44 -117
  44. data/lib/sidekiq/ring_buffer.rb +2 -0
  45. data/lib/sidekiq/scheduled.rb +62 -35
  46. data/lib/sidekiq/systemd.rb +2 -0
  47. data/lib/sidekiq/testing.rb +37 -46
  48. data/lib/sidekiq/transaction_aware_client.rb +11 -5
  49. data/lib/sidekiq/version.rb +6 -1
  50. data/lib/sidekiq/web/action.rb +15 -5
  51. data/lib/sidekiq/web/application.rb +94 -24
  52. data/lib/sidekiq/web/csrf_protection.rb +10 -7
  53. data/lib/sidekiq/web/helpers.rb +118 -45
  54. data/lib/sidekiq/web/router.rb +5 -2
  55. data/lib/sidekiq/web.rb +67 -15
  56. data/lib/sidekiq/worker_compatibility_alias.rb +13 -0
  57. data/lib/sidekiq.rb +78 -266
  58. data/sidekiq.gemspec +12 -10
  59. data/web/assets/javascripts/application.js +46 -1
  60. data/web/assets/javascripts/base-charts.js +106 -0
  61. data/web/assets/javascripts/chart.min.js +13 -0
  62. data/web/assets/javascripts/chartjs-plugin-annotation.min.js +7 -0
  63. data/web/assets/javascripts/dashboard-charts.js +192 -0
  64. data/web/assets/javascripts/dashboard.js +11 -250
  65. data/web/assets/javascripts/metrics.js +298 -0
  66. data/web/assets/stylesheets/application-dark.css +4 -0
  67. data/web/assets/stylesheets/application-rtl.css +10 -89
  68. data/web/assets/stylesheets/application.css +98 -295
  69. data/web/locales/ar.yml +70 -70
  70. data/web/locales/cs.yml +62 -62
  71. data/web/locales/da.yml +60 -53
  72. data/web/locales/de.yml +65 -65
  73. data/web/locales/el.yml +43 -24
  74. data/web/locales/en.yml +83 -69
  75. data/web/locales/es.yml +68 -68
  76. data/web/locales/fa.yml +65 -65
  77. data/web/locales/fr.yml +80 -67
  78. data/web/locales/gd.yml +98 -0
  79. data/web/locales/he.yml +65 -64
  80. data/web/locales/hi.yml +59 -59
  81. data/web/locales/it.yml +85 -54
  82. data/web/locales/ja.yml +72 -68
  83. data/web/locales/ko.yml +52 -52
  84. data/web/locales/lt.yml +66 -66
  85. data/web/locales/nb.yml +61 -61
  86. data/web/locales/nl.yml +52 -52
  87. data/web/locales/pl.yml +45 -45
  88. data/web/locales/pt-br.yml +78 -69
  89. data/web/locales/pt.yml +51 -51
  90. data/web/locales/ru.yml +67 -66
  91. data/web/locales/sv.yml +53 -53
  92. data/web/locales/ta.yml +60 -60
  93. data/web/locales/tr.yml +100 -0
  94. data/web/locales/uk.yml +85 -61
  95. data/web/locales/ur.yml +64 -64
  96. data/web/locales/vi.yml +67 -67
  97. data/web/locales/zh-cn.yml +42 -16
  98. data/web/locales/zh-tw.yml +41 -8
  99. data/web/views/_footer.erb +17 -2
  100. data/web/views/_job_info.erb +18 -2
  101. data/web/views/_metrics_period_select.erb +12 -0
  102. data/web/views/_nav.erb +1 -1
  103. data/web/views/_paging.erb +2 -0
  104. data/web/views/_poll_link.erb +1 -1
  105. data/web/views/_summary.erb +7 -7
  106. data/web/views/busy.erb +49 -33
  107. data/web/views/dashboard.erb +28 -6
  108. data/web/views/filtering.erb +6 -0
  109. data/web/views/layout.erb +6 -6
  110. data/web/views/metrics.erb +90 -0
  111. data/web/views/metrics_for_job.erb +59 -0
  112. data/web/views/morgue.erb +5 -9
  113. data/web/views/queue.erb +15 -15
  114. data/web/views/queues.erb +9 -3
  115. data/web/views/retries.erb +5 -9
  116. data/web/views/scheduled.erb +12 -13
  117. metadata +61 -26
  118. data/lib/sidekiq/.DS_Store +0 -0
  119. data/lib/sidekiq/delay.rb +0 -43
  120. data/lib/sidekiq/extensions/action_mailer.rb +0 -48
  121. data/lib/sidekiq/extensions/active_record.rb +0 -43
  122. data/lib/sidekiq/extensions/class_methods.rb +0 -43
  123. data/lib/sidekiq/extensions/generic_proxy.rb +0 -33
  124. data/lib/sidekiq/worker.rb +0 -367
  125. /data/{LICENSE → LICENSE.txt} +0 -0
@@ -0,0 +1,106 @@
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.
29
+ class Histogram
30
+ include Enumerable
31
+
32
+ # This number represents the maximum milliseconds for this bucket.
33
+ # 20 means all job executions up to 20ms, e.g. if a job takes
34
+ # 280ms, it'll increment bucket[7]. Note we can track job executions
35
+ # up to about 5.5 minutes. After that, it's assumed you're probably
36
+ # not too concerned with its performance.
37
+ BUCKET_INTERVALS = [
38
+ 20, 30, 45, 65, 100,
39
+ 150, 225, 335, 500, 750,
40
+ 1100, 1700, 2500, 3800, 5750,
41
+ 8500, 13000, 20000, 30000, 45000,
42
+ 65000, 100000, 150000, 225000, 335000,
43
+ 1e20 # the "maybe your job is too long" bucket
44
+ ].freeze
45
+ LABELS = [
46
+ "20ms", "30ms", "45ms", "65ms", "100ms",
47
+ "150ms", "225ms", "335ms", "500ms", "750ms",
48
+ "1.1s", "1.7s", "2.5s", "3.8s", "5.75s",
49
+ "8.5s", "13s", "20s", "30s", "45s",
50
+ "65s", "100s", "150s", "225s", "335s",
51
+ "Slow"
52
+ ].freeze
53
+ FETCH = "GET u16 #0 GET u16 #1 GET u16 #2 GET u16 #3 \
54
+ GET u16 #4 GET u16 #5 GET u16 #6 GET u16 #7 \
55
+ GET u16 #8 GET u16 #9 GET u16 #10 GET u16 #11 \
56
+ GET u16 #12 GET u16 #13 GET u16 #14 GET u16 #15 \
57
+ GET u16 #16 GET u16 #17 GET u16 #18 GET u16 #19 \
58
+ GET u16 #20 GET u16 #21 GET u16 #22 GET u16 #23 \
59
+ GET u16 #24 GET u16 #25".split
60
+ HISTOGRAM_TTL = 8 * 60 * 60
61
+
62
+ def each
63
+ buckets.each { |counter| yield counter.value }
64
+ end
65
+
66
+ def label(idx)
67
+ LABELS[idx]
68
+ end
69
+
70
+ attr_reader :buckets
71
+ def initialize(klass)
72
+ @klass = klass
73
+ @buckets = Array.new(BUCKET_INTERVALS.size) { Counter.new }
74
+ end
75
+
76
+ def record_time(ms)
77
+ index_to_use = BUCKET_INTERVALS.each_index do |idx|
78
+ break idx if ms < BUCKET_INTERVALS[idx]
79
+ end
80
+
81
+ @buckets[index_to_use].increment
82
+ end
83
+
84
+ def fetch(conn, now = Time.now)
85
+ window = now.utc.strftime("%d-%H:%-M")
86
+ key = "#{@klass}-#{window}"
87
+ conn.bitfield_ro(key, *FETCH)
88
+ end
89
+
90
+ def persist(conn, now = Time.now)
91
+ buckets, @buckets = @buckets, []
92
+ window = now.utc.strftime("%d-%H:%-M")
93
+ key = "#{@klass}-#{window}"
94
+ cmd = [key, "OVERFLOW", "SAT"]
95
+ buckets.each_with_index do |counter, idx|
96
+ val = counter.value
97
+ cmd << "INCRBY" << "u16" << "##{idx}" << val.to_s if val > 0
98
+ end
99
+
100
+ conn.bitfield(*cmd) if cmd.size > 3
101
+ conn.expire(key, HISTOGRAM_TTL)
102
+ key
103
+ end
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,148 @@
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 = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC, :millisecond)
23
+ time_ms = 0
24
+ begin
25
+ begin
26
+ yield
27
+ ensure
28
+ finish = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC, :millisecond)
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 = 7 * 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
+ # nowhour = now.strftime("%Y%m%d|%-H")
66
+ nowmin = now.strftime("%Y%m%d|%-H:%-M")
67
+ count = 0
68
+
69
+ redis do |conn|
70
+ # persist fine-grained histogram data
71
+ if grams.size > 0
72
+ conn.pipelined do |pipe|
73
+ grams.each do |_, gram|
74
+ gram.persist(pipe, now)
75
+ end
76
+ end
77
+ end
78
+
79
+ # persist coarse grained execution count + execution millis.
80
+ # note as of today we don't use or do anything with the
81
+ # daily or hourly rollups.
82
+ [
83
+ # ["j", jobs, nowdate, LONG_TERM],
84
+ # ["j", jobs, nowhour, MID_TERM],
85
+ ["j", jobs, nowmin, SHORT_TERM]
86
+ ].each do |prefix, data, bucket, ttl|
87
+ conn.pipelined do |xa|
88
+ stats = "#{prefix}|#{bucket}"
89
+ data.each_pair do |key, value|
90
+ xa.hincrby stats, key, value
91
+ count += 1
92
+ end
93
+ xa.expire(stats, ttl)
94
+ end
95
+ end
96
+ logger.debug "Flushed #{count} metrics"
97
+ count
98
+ end
99
+ end
100
+
101
+ private
102
+
103
+ def track_time(klass, time_ms)
104
+ @lock.synchronize {
105
+ @grams[klass].record_time(time_ms)
106
+ @jobs["#{klass}|ms"] += time_ms
107
+ @totals["ms"] += time_ms
108
+ }
109
+ end
110
+
111
+ def reset
112
+ @lock.synchronize {
113
+ array = [@totals, @jobs, @grams]
114
+ reset_instance_variables
115
+ array
116
+ }
117
+ end
118
+
119
+ def reset_instance_variables
120
+ @totals = Hash.new(0)
121
+ @jobs = Hash.new(0)
122
+ @grams = Hash.new { |hash, key| hash[key] = Histogram.new(key) }
123
+ end
124
+ end
125
+
126
+ class Middleware
127
+ include Sidekiq::ServerMiddleware
128
+
129
+ def initialize(options)
130
+ @exec = options
131
+ end
132
+
133
+ def call(_instance, hash, queue, &block)
134
+ @exec.track(queue, hash["wrapped"] || hash["class"], &block)
135
+ end
136
+ end
137
+ end
138
+ end
139
+
140
+ Sidekiq.configure_server do |config|
141
+ exec = Sidekiq::Metrics::ExecutionTracker.new(config)
142
+ config.server_middleware do |chain|
143
+ chain.add Sidekiq::Metrics::Middleware, exec
144
+ end
145
+ config.on(:beat) do
146
+ exec.flush
147
+ end
148
+ end
@@ -4,84 +4,89 @@ require "sidekiq/middleware/modules"
4
4
 
5
5
  module Sidekiq
6
6
  # Middleware is code configured to run before/after
7
- # a message is processed. It is patterned after Rack
7
+ # a job is processed. It is patterned after Rack
8
8
  # middleware. Middleware exists for the client side
9
9
  # (pushing jobs onto the queue) as well as the server
10
10
  # side (when jobs are actually processed).
11
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
+ #
12
17
  # To add middleware for the client:
13
18
  #
14
- # Sidekiq.configure_client do |config|
15
- # config.client_middleware do |chain|
16
- # chain.add MyClientHook
19
+ # Sidekiq.configure_client do |config|
20
+ # config.client_middleware do |chain|
21
+ # chain.add MyClientHook
22
+ # end
17
23
  # end
18
- # end
19
24
  #
20
25
  # To modify middleware for the server, just call
21
26
  # with another block:
22
27
  #
23
- # Sidekiq.configure_server do |config|
24
- # config.server_middleware do |chain|
25
- # chain.add MyServerHook
26
- # 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
27
33
  # end
28
- # end
29
34
  #
30
35
  # To insert immediately preceding another entry:
31
36
  #
32
- # Sidekiq.configure_client do |config|
33
- # config.client_middleware do |chain|
34
- # 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
35
41
  # end
36
- # end
37
42
  #
38
43
  # To insert immediately after another entry:
39
44
  #
40
- # Sidekiq.configure_client do |config|
41
- # config.client_middleware do |chain|
42
- # 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
43
49
  # end
44
- # end
45
50
  #
46
51
  # This is an example of a minimal server middleware:
47
52
  #
48
- # class MyServerHook
49
- # include Sidekiq::ServerMiddleware
50
- # def call(job_instance, msg, queue)
51
- # logger.info "Before job"
52
- # redis {|conn| conn.get("foo") } # do something in Redis
53
- # yield
54
- # logger.info "After job"
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
55
62
  # end
56
- # end
57
63
  #
58
64
  # This is an example of a minimal client middleware, note
59
65
  # the method must return the result or the job will not push
60
66
  # to Redis:
61
67
  #
62
- # class MyClientHook
63
- # include Sidekiq::ClientMiddleware
64
- # def call(job_class, msg, queue, redis_pool)
65
- # logger.info "Before push"
66
- # result = yield
67
- # logger.info "After push"
68
- # 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
69
77
  # end
70
- # end
71
78
  #
72
79
  module Middleware
73
80
  class Chain
74
81
  include Enumerable
75
82
 
76
- def initialize_copy(copy)
77
- copy.instance_variable_set(:@entries, entries.dup)
78
- end
79
-
83
+ # Iterate through each middleware in the chain
80
84
  def each(&block)
81
85
  entries.each(&block)
82
86
  end
83
87
 
84
- def initialize(config = nil)
88
+ # @api private
89
+ def initialize(config = nil) # :nodoc:
85
90
  @config = config
86
91
  @entries = nil
87
92
  yield self if block_given?
@@ -91,20 +96,39 @@ module Sidekiq
91
96
  @entries ||= []
92
97
  end
93
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]
94
107
  def remove(klass)
95
108
  entries.delete_if { |entry| entry.klass == klass }
96
109
  end
97
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
98
119
  def add(klass, *args)
99
120
  remove(klass)
100
121
  entries << Entry.new(@config, klass, *args)
101
122
  end
102
123
 
124
+ # Identical to {#add} except the middleware is added to the front of the chain.
103
125
  def prepend(klass, *args)
104
126
  remove(klass)
105
127
  entries.insert(0, Entry.new(@config, klass, *args))
106
128
  end
107
129
 
130
+ # Inserts +newklass+ before +oldklass+ in the chain.
131
+ # Useful if one middleware must run before another middleware.
108
132
  def insert_before(oldklass, newklass, *args)
109
133
  i = entries.index { |entry| entry.klass == newklass }
110
134
  new_entry = i.nil? ? Entry.new(@config, newklass, *args) : entries.delete_at(i)
@@ -112,6 +136,8 @@ module Sidekiq
112
136
  entries.insert(i, new_entry)
113
137
  end
114
138
 
139
+ # Inserts +newklass+ after +oldklass+ in the chain.
140
+ # Useful if one middleware must run after another middleware.
115
141
  def insert_after(oldklass, newklass, *args)
116
142
  i = entries.index { |entry| entry.klass == newklass }
117
143
  new_entry = i.nil? ? Entry.new(@config, newklass, *args) : entries.delete_at(i)
@@ -119,10 +145,13 @@ module Sidekiq
119
145
  entries.insert(i + 1, new_entry)
120
146
  end
121
147
 
148
+ # @return [Boolean] if the given class is already in the chain
122
149
  def exists?(klass)
123
150
  any? { |entry| entry.klass == klass }
124
151
  end
152
+ alias_method :include?, :exists?
125
153
 
154
+ # @return [Boolean] if the chain contains no middleware
126
155
  def empty?
127
156
  @entries.nil? || @entries.empty?
128
157
  end
@@ -135,23 +164,30 @@ module Sidekiq
135
164
  entries.clear
136
165
  end
137
166
 
138
- def invoke(*args)
167
+ # Used by Sidekiq to execute the middleware at runtime
168
+ # @api private
169
+ def invoke(*args, &block)
139
170
  return yield if empty?
140
171
 
141
172
  chain = retrieve
142
- traverse_chain = proc do
143
- if chain.empty?
144
- yield
145
- else
146
- 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)
147
184
  end
148
185
  end
149
- traverse_chain.call
150
186
  end
151
187
  end
152
188
 
153
- private
154
-
189
+ # Represents each link in the middleware chain
190
+ # @api private
155
191
  class Entry
156
192
  attr_reader :klass
157
193
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "active_support/current_attributes"
2
4
 
3
5
  module Sidekiq
@@ -7,54 +9,119 @@ module Sidekiq
7
9
  # This can be useful for multi-tenancy, i18n locale, timezone, any implicit
8
10
  # per-request attribute. See +ActiveSupport::CurrentAttributes+.
9
11
  #
12
+ # For multiple current attributes, pass an array of current attributes.
13
+ #
10
14
  # @example
11
15
  #
12
16
  # # in your initializer
13
17
  # require "sidekiq/middleware/current_attributes"
14
- # Sidekiq::CurrentAttributes.persist(Myapp::Current)
18
+ # Sidekiq::CurrentAttributes.persist("Myapp::Current")
19
+ # # or multiple current attributes
20
+ # Sidekiq::CurrentAttributes.persist(["Myapp::Current", "Myapp::OtherCurrent"])
15
21
  #
16
22
  module CurrentAttributes
17
23
  class Save
18
24
  include Sidekiq::ClientMiddleware
19
25
 
20
- def initialize(cattr)
21
- @klass = cattr
26
+ def initialize(cattrs)
27
+ @cattrs = cattrs
22
28
  end
23
29
 
24
30
  def call(_, job, _, _)
25
- attrs = @klass.attributes
26
- if job.has_key?("cattr")
27
- job["cattr"].merge!(attrs)
28
- else
29
- job["cattr"] = attrs
31
+ @cattrs.each do |(key, strklass)|
32
+ if !job.has_key?(key)
33
+ attrs = strklass.constantize.attributes
34
+ # Retries can push the job N times, we don't
35
+ # want retries to reset cattr. #5692, #5090
36
+ if attrs.any?
37
+ # Older rails has a bug that `CurrentAttributes#attributes` always returns
38
+ # the same hash instance. We need to dup it to avoid being accidentally mutated.
39
+ job[key] = if returns_same_object?
40
+ attrs.dup
41
+ else
42
+ attrs
43
+ end
44
+ end
45
+ end
30
46
  end
31
47
  yield
32
48
  end
49
+
50
+ private
51
+
52
+ def returns_same_object?
53
+ ActiveSupport::VERSION::MAJOR < 8 ||
54
+ (ActiveSupport::VERSION::MAJOR == 8 && ActiveSupport::VERSION::MINOR == 0)
55
+ end
33
56
  end
34
57
 
35
58
  class Load
36
59
  include Sidekiq::ServerMiddleware
37
60
 
38
- def initialize(cattr)
39
- @klass = cattr
61
+ def initialize(cattrs)
62
+ @cattrs = cattrs
40
63
  end
41
64
 
42
65
  def call(_, job, _, &block)
43
- if job.has_key?("cattr")
44
- @klass.set(job["cattr"], &block)
45
- else
46
- yield
66
+ klass_attrs = {}
67
+
68
+ @cattrs.each do |(key, strklass)|
69
+ next unless job.has_key?(key)
70
+
71
+ klass_attrs[strklass.constantize] = job[key]
72
+ end
73
+
74
+ wrap(klass_attrs.to_a, &block)
75
+ end
76
+
77
+ private
78
+
79
+ def wrap(klass_attrs, &block)
80
+ klass, attrs = klass_attrs.shift
81
+ return block.call unless klass
82
+
83
+ retried = false
84
+
85
+ begin
86
+ klass.set(attrs) do
87
+ wrap(klass_attrs, &block)
88
+ end
89
+ rescue NoMethodError
90
+ raise if retried
91
+
92
+ # It is possible that the `CurrentAttributes` definition
93
+ # was changed before the job started processing.
94
+ attrs = attrs.select { |attr| klass.respond_to?(attr) }
95
+ retried = true
96
+ retry
47
97
  end
48
98
  end
49
99
  end
50
100
 
51
- def self.persist(klass)
52
- Sidekiq.configure_client do |config|
53
- config.client_middleware.add Save, klass
101
+ class << self
102
+ def persist(klass_or_array, config = Sidekiq.default_configuration)
103
+ cattrs = build_cattrs_hash(klass_or_array)
104
+
105
+ config.client_middleware.add Save, cattrs
106
+ config.server_middleware.prepend Load, cattrs
54
107
  end
55
- Sidekiq.configure_server do |config|
56
- config.client_middleware.add Save, klass
57
- config.server_middleware.add Load, klass
108
+
109
+ private
110
+
111
+ def build_cattrs_hash(klass_or_array)
112
+ if klass_or_array.is_a?(Array)
113
+ {}.tap do |hash|
114
+ klass_or_array.each_with_index do |klass, index|
115
+ hash[key_at(index)] = klass.to_s
116
+ end
117
+ end
118
+ else
119
+ {key_at(0) => klass_or_array.to_s}
120
+ end
121
+ end
122
+
123
+ def key_at(index)
124
+ (index == 0) ? "cattr" : "cattr_#{index}"
58
125
  end
59
126
  end
60
127
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Sidekiq
2
4
  # Server-side middleware must import this Module in order
3
5
  # to get access to server resources during `call`.
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
2
3
 
3
4
  require "fileutils"
4
5
  require "sidekiq/api"
@@ -16,8 +17,6 @@ class Sidekiq::Monitor
16
17
  return
17
18
  end
18
19
  send(section)
19
- rescue => e
20
- abort "Couldn't get status: #{e}"
21
20
  end
22
21
 
23
22
  def all
@@ -49,10 +48,25 @@ class Sidekiq::Monitor
49
48
  def processes
50
49
  puts "---- Processes (#{process_set.size}) ----"
51
50
  process_set.each_with_index do |process, index|
51
+ # Keep compatibility with legacy versions since we don't want to break sidekiqmon during rolling upgrades or downgrades.
52
+ #
53
+ # Before:
54
+ # ["default", "critical"]
55
+ #
56
+ # After:
57
+ # {"default" => 1, "critical" => 10}
58
+ queues =
59
+ if process["weights"]
60
+ process["weights"].sort_by { |queue| queue[0] }.map { |capsule| capsule.map { |name, weight| (weight > 0) ? "#{name}: #{weight}" : name }.join(", ") }
61
+ else
62
+ process["queues"].sort
63
+ end
64
+
52
65
  puts "#{process["identity"]} #{tags_for(process)}"
53
66
  puts " Started: #{Time.at(process["started_at"])} (#{time_ago(process["started_at"])})"
54
67
  puts " Threads: #{process["concurrency"]} (#{process["busy"]} busy)"
55
- puts " Queues: #{split_multiline(process["queues"].sort, pad: 11)}"
68
+ puts " Queues: #{split_multiline(queues, pad: 11)}"
69
+ puts " Version: #{process["version"] || "Unknown"}" if process["version"] != Sidekiq::VERSION
56
70
  puts "" unless (index + 1) == process_set.size
57
71
  end
58
72
  end
@@ -85,7 +99,7 @@ class Sidekiq::Monitor
85
99
  pad = opts[:pad] || 0
86
100
  max_length = opts[:max_length] || (80 - pad)
87
101
  out = []
88
- line = ""
102
+ line = +""
89
103
  values.each do |value|
90
104
  if (line.length + value.length) > max_length
91
105
  out << line
@@ -101,7 +115,7 @@ class Sidekiq::Monitor
101
115
  tags = [
102
116
  process["tag"],
103
117
  process["labels"],
104
- (process["quiet"] == "true" ? "quiet" : nil)
118
+ ((process["quiet"] == "true") ? "quiet" : nil)
105
119
  ].flatten.compact
106
120
  tags.any? ? "[#{tags.join("] [")}]" : nil
107
121
  end