sidekiq 6.3.1 → 7.1.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (120) hide show
  1. checksums.yaml +4 -4
  2. data/Changes.md +285 -11
  3. data/LICENSE.txt +9 -0
  4. data/README.md +47 -34
  5. data/bin/sidekiq +4 -9
  6. data/bin/sidekiqload +207 -117
  7. data/bin/sidekiqmon +4 -1
  8. data/lib/generators/sidekiq/job_generator.rb +57 -0
  9. data/lib/generators/sidekiq/templates/{worker.rb.erb → job.rb.erb} +2 -2
  10. data/lib/generators/sidekiq/templates/{worker_spec.rb.erb → job_spec.rb.erb} +1 -1
  11. data/lib/generators/sidekiq/templates/{worker_test.rb.erb → job_test.rb.erb} +1 -1
  12. data/lib/sidekiq/api.rb +333 -190
  13. data/lib/sidekiq/capsule.rb +127 -0
  14. data/lib/sidekiq/cli.rb +86 -80
  15. data/lib/sidekiq/client.rb +104 -95
  16. data/lib/sidekiq/{util.rb → component.rb} +14 -41
  17. data/lib/sidekiq/config.rb +282 -0
  18. data/lib/sidekiq/deploy.rb +62 -0
  19. data/lib/sidekiq/embedded.rb +61 -0
  20. data/lib/sidekiq/fetch.rb +23 -24
  21. data/lib/sidekiq/job.rb +371 -10
  22. data/lib/sidekiq/job_logger.rb +16 -28
  23. data/lib/sidekiq/job_retry.rb +99 -58
  24. data/lib/sidekiq/job_util.rb +107 -0
  25. data/lib/sidekiq/launcher.rb +103 -95
  26. data/lib/sidekiq/logger.rb +9 -44
  27. data/lib/sidekiq/manager.rb +40 -41
  28. data/lib/sidekiq/metrics/query.rb +153 -0
  29. data/lib/sidekiq/metrics/shared.rb +95 -0
  30. data/lib/sidekiq/metrics/tracking.rb +136 -0
  31. data/lib/sidekiq/middleware/chain.rb +96 -51
  32. data/lib/sidekiq/middleware/current_attributes.rb +59 -16
  33. data/lib/sidekiq/middleware/i18n.rb +6 -4
  34. data/lib/sidekiq/middleware/modules.rb +21 -0
  35. data/lib/sidekiq/monitor.rb +17 -4
  36. data/lib/sidekiq/paginator.rb +17 -9
  37. data/lib/sidekiq/processor.rb +81 -80
  38. data/lib/sidekiq/rails.rb +21 -14
  39. data/lib/sidekiq/redis_client_adapter.rb +95 -0
  40. data/lib/sidekiq/redis_connection.rb +14 -82
  41. data/lib/sidekiq/ring_buffer.rb +29 -0
  42. data/lib/sidekiq/scheduled.rb +76 -38
  43. data/lib/sidekiq/testing/inline.rb +4 -4
  44. data/lib/sidekiq/testing.rb +42 -69
  45. data/lib/sidekiq/transaction_aware_client.rb +44 -0
  46. data/lib/sidekiq/version.rb +2 -1
  47. data/lib/sidekiq/web/action.rb +3 -3
  48. data/lib/sidekiq/web/application.rb +95 -11
  49. data/lib/sidekiq/web/csrf_protection.rb +4 -4
  50. data/lib/sidekiq/web/helpers.rb +58 -30
  51. data/lib/sidekiq/web.rb +22 -17
  52. data/lib/sidekiq/worker_compatibility_alias.rb +13 -0
  53. data/lib/sidekiq.rb +85 -202
  54. data/sidekiq.gemspec +12 -10
  55. data/web/assets/javascripts/application.js +77 -26
  56. data/web/assets/javascripts/base-charts.js +106 -0
  57. data/web/assets/javascripts/chart.min.js +13 -0
  58. data/web/assets/javascripts/chartjs-plugin-annotation.min.js +7 -0
  59. data/web/assets/javascripts/dashboard-charts.js +168 -0
  60. data/web/assets/javascripts/dashboard.js +3 -240
  61. data/web/assets/javascripts/metrics.js +264 -0
  62. data/web/assets/stylesheets/application-dark.css +17 -17
  63. data/web/assets/stylesheets/application-rtl.css +2 -91
  64. data/web/assets/stylesheets/application.css +69 -302
  65. data/web/locales/ar.yml +70 -70
  66. data/web/locales/cs.yml +62 -62
  67. data/web/locales/da.yml +60 -53
  68. data/web/locales/de.yml +65 -65
  69. data/web/locales/el.yml +43 -24
  70. data/web/locales/en.yml +84 -69
  71. data/web/locales/es.yml +68 -68
  72. data/web/locales/fa.yml +65 -65
  73. data/web/locales/fr.yml +81 -67
  74. data/web/locales/gd.yml +99 -0
  75. data/web/locales/he.yml +65 -64
  76. data/web/locales/hi.yml +59 -59
  77. data/web/locales/it.yml +53 -53
  78. data/web/locales/ja.yml +73 -68
  79. data/web/locales/ko.yml +52 -52
  80. data/web/locales/lt.yml +66 -66
  81. data/web/locales/nb.yml +61 -61
  82. data/web/locales/nl.yml +52 -52
  83. data/web/locales/pl.yml +45 -45
  84. data/web/locales/pt-br.yml +83 -55
  85. data/web/locales/pt.yml +51 -51
  86. data/web/locales/ru.yml +67 -66
  87. data/web/locales/sv.yml +53 -53
  88. data/web/locales/ta.yml +60 -60
  89. data/web/locales/uk.yml +62 -61
  90. data/web/locales/ur.yml +64 -64
  91. data/web/locales/vi.yml +67 -67
  92. data/web/locales/zh-cn.yml +43 -16
  93. data/web/locales/zh-tw.yml +42 -8
  94. data/web/views/_footer.erb +5 -2
  95. data/web/views/_job_info.erb +18 -2
  96. data/web/views/_metrics_period_select.erb +12 -0
  97. data/web/views/_nav.erb +1 -1
  98. data/web/views/_paging.erb +2 -0
  99. data/web/views/_poll_link.erb +1 -1
  100. data/web/views/_summary.erb +1 -1
  101. data/web/views/busy.erb +44 -28
  102. data/web/views/dashboard.erb +36 -4
  103. data/web/views/filtering.erb +7 -0
  104. data/web/views/metrics.erb +82 -0
  105. data/web/views/metrics_for_job.erb +68 -0
  106. data/web/views/morgue.erb +5 -9
  107. data/web/views/queue.erb +15 -15
  108. data/web/views/queues.erb +3 -1
  109. data/web/views/retries.erb +5 -9
  110. data/web/views/scheduled.erb +12 -13
  111. metadata +62 -31
  112. data/LICENSE +0 -9
  113. data/lib/generators/sidekiq/worker_generator.rb +0 -57
  114. data/lib/sidekiq/delay.rb +0 -41
  115. data/lib/sidekiq/exception_handler.rb +0 -27
  116. data/lib/sidekiq/extensions/action_mailer.rb +0 -48
  117. data/lib/sidekiq/extensions/active_record.rb +0 -43
  118. data/lib/sidekiq/extensions/class_methods.rb +0 -43
  119. data/lib/sidekiq/extensions/generic_proxy.rb +0 -33
  120. data/lib/sidekiq/worker.rb +0 -311
@@ -0,0 +1,153 @@
1
+ require "sidekiq"
2
+ require "date"
3
+ require "set"
4
+
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 specifically 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
+ # Get metric data for all jobs from the last hour
23
+ def top_jobs(minutes: 60)
24
+ result = Result.new
25
+
26
+ time = @time
27
+ redis_results = @pool.with do |conn|
28
+ conn.pipelined do |pipe|
29
+ minutes.times do |idx|
30
+ key = "j|#{time.strftime("%Y%m%d")}|#{time.hour}:#{time.min}"
31
+ pipe.hgetall key
32
+ result.prepend_bucket time
33
+ time -= 60
34
+ end
35
+ end
36
+ end
37
+
38
+ time = @time
39
+ redis_results.each do |hash|
40
+ hash.each do |k, v|
41
+ kls, metric = k.split("|")
42
+ result.job_results[kls].add_metric metric, time, v.to_i
43
+ end
44
+ time -= 60
45
+ end
46
+
47
+ result.marks = fetch_marks(result.starts_at..result.ends_at)
48
+
49
+ result
50
+ end
51
+
52
+ def for_job(klass, minutes: 60)
53
+ result = Result.new
54
+
55
+ time = @time
56
+ redis_results = @pool.with do |conn|
57
+ conn.pipelined do |pipe|
58
+ minutes.times do |idx|
59
+ key = "j|#{time.strftime("%Y%m%d")}|#{time.hour}:#{time.min}"
60
+ pipe.hmget key, "#{klass}|ms", "#{klass}|p", "#{klass}|f"
61
+ result.prepend_bucket time
62
+ time -= 60
63
+ end
64
+ end
65
+ end
66
+
67
+ time = @time
68
+ @pool.with do |conn|
69
+ redis_results.each do |(ms, p, f)|
70
+ result.job_results[klass].add_metric "ms", time, ms.to_i if ms
71
+ result.job_results[klass].add_metric "p", time, p.to_i if p
72
+ 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).reverse
74
+ time -= 60
75
+ end
76
+ end
77
+
78
+ result.marks = fetch_marks(result.starts_at..result.ends_at)
79
+
80
+ result
81
+ end
82
+
83
+ class Result < Struct.new(:starts_at, :ends_at, :size, :buckets, :job_results, :marks)
84
+ def initialize
85
+ super
86
+ self.buckets = []
87
+ 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
95
+ end
96
+ end
97
+
98
+ class JobResult < Struct.new(:series, :hist, :totals)
99
+ def initialize
100
+ super
101
+ self.series = Hash.new { |h, k| h[k] = Hash.new(0) }
102
+ self.hist = Hash.new { |h, k| h[k] = [] }
103
+ self.totals = Hash.new(0)
104
+ end
105
+
106
+ def add_metric(metric, time, value)
107
+ totals[metric] += value
108
+ series[metric][time.strftime("%H:%M")] += value
109
+
110
+ # Include timing measurements in seconds for convenience
111
+ add_metric("s", time, value / 1000.0) if metric == "ms"
112
+ end
113
+
114
+ def add_hist(time, hist_result)
115
+ hist[time.strftime("%H:%M")] = hist_result
116
+ end
117
+
118
+ def total_avg(metric = "ms")
119
+ completed = totals["p"] - totals["f"]
120
+ totals[metric].to_f / completed
121
+ end
122
+
123
+ def series_avg(metric = "ms")
124
+ series[metric].each_with_object(Hash.new(0)) do |(bucket, value), result|
125
+ completed = series.dig("p", bucket) - series.dig("f", bucket)
126
+ result[bucket] = (completed == 0) ? 0 : value.to_f / completed
127
+ end
128
+ end
129
+ end
130
+
131
+ class MarkResult < Struct.new(:time, :label)
132
+ def bucket
133
+ time.strftime("%H:%M")
134
+ end
135
+ end
136
+
137
+ private
138
+
139
+ def fetch_marks(time_range)
140
+ [].tap do |result|
141
+ marks = @pool.with { |c| c.hgetall("#{@time.strftime("%Y%m%d")}-marks") }
142
+
143
+ marks.each do |timestamp, label|
144
+ time = Time.parse(timestamp)
145
+ if time_range.cover? time
146
+ result << MarkResult.new(time, label)
147
+ end
148
+ end
149
+ end
150
+ end
151
+ end
152
+ end
153
+ end
@@ -0,0 +1,95 @@
1
+ require "concurrent"
2
+
3
+ module Sidekiq
4
+ module Metrics
5
+ # This is the only dependency on concurrent-ruby in Sidekiq but it's
6
+ # mandatory for thread-safety until MRI supports atomic operations on values.
7
+ Counter = ::Concurrent::AtomicFixnum
8
+
9
+ # Implements space-efficient but statistically useful histogram storage.
10
+ # A precise time histogram stores every time. Instead we break times into a set of
11
+ # known buckets and increment counts of the associated time bucket. Even if we call
12
+ # the histogram a million times, we'll still only store 26 buckets.
13
+ # NB: needs to be thread-safe or resiliant to races.
14
+ #
15
+ # To store this data, we use Redis' BITFIELD command to store unsigned 16-bit counters
16
+ # per bucket per klass per minute. It's unlikely that most people will be executing more
17
+ # than 1000 job/sec for a full minute of a specific type.
18
+ class Histogram
19
+ include Enumerable
20
+
21
+ # This number represents the maximum milliseconds for this bucket.
22
+ # 20 means all job executions up to 20ms, e.g. if a job takes
23
+ # 280ms, it'll increment bucket[7]. Note we can track job executions
24
+ # up to about 5.5 minutes. After that, it's assumed you're probably
25
+ # not too concerned with its performance.
26
+ BUCKET_INTERVALS = [
27
+ 20, 30, 45, 65, 100,
28
+ 150, 225, 335, 500, 750,
29
+ 1100, 1700, 2500, 3800, 5750,
30
+ 8500, 13000, 20000, 30000, 45000,
31
+ 65000, 100000, 150000, 225000, 335000,
32
+ 1e20 # the "maybe your job is too long" bucket
33
+ ].freeze
34
+ LABELS = [
35
+ "20ms", "30ms", "45ms", "65ms", "100ms",
36
+ "150ms", "225ms", "335ms", "500ms", "750ms",
37
+ "1.1s", "1.7s", "2.5s", "3.8s", "5.75s",
38
+ "8.5s", "13s", "20s", "30s", "45s",
39
+ "65s", "100s", "150s", "225s", "335s",
40
+ "Slow"
41
+ ].freeze
42
+ FETCH = "GET u16 #0 GET u16 #1 GET u16 #2 GET u16 #3 \
43
+ GET u16 #4 GET u16 #5 GET u16 #6 GET u16 #7 \
44
+ GET u16 #8 GET u16 #9 GET u16 #10 GET u16 #11 \
45
+ GET u16 #12 GET u16 #13 GET u16 #14 GET u16 #15 \
46
+ GET u16 #16 GET u16 #17 GET u16 #18 GET u16 #19 \
47
+ GET u16 #20 GET u16 #21 GET u16 #22 GET u16 #23 \
48
+ GET u16 #24 GET u16 #25".split
49
+ HISTOGRAM_TTL = 8 * 60 * 60
50
+
51
+ def each
52
+ buckets.each { |counter| yield counter.value }
53
+ end
54
+
55
+ def label(idx)
56
+ LABELS[idx]
57
+ end
58
+
59
+ attr_reader :buckets
60
+ def initialize(klass)
61
+ @klass = klass
62
+ @buckets = Array.new(BUCKET_INTERVALS.size) { Counter.new }
63
+ end
64
+
65
+ def record_time(ms)
66
+ index_to_use = BUCKET_INTERVALS.each_index do |idx|
67
+ break idx if ms < BUCKET_INTERVALS[idx]
68
+ end
69
+
70
+ @buckets[index_to_use].increment
71
+ end
72
+
73
+ def fetch(conn, now = Time.now)
74
+ window = now.utc.strftime("%d-%H:%-M")
75
+ key = "#{@klass}-#{window}"
76
+ conn.bitfield_ro(key, *FETCH)
77
+ end
78
+
79
+ def persist(conn, now = Time.now)
80
+ buckets, @buckets = @buckets, []
81
+ window = now.utc.strftime("%d-%H:%-M")
82
+ key = "#{@klass}-#{window}"
83
+ cmd = [key, "OVERFLOW", "SAT"]
84
+ buckets.each_with_index do |counter, idx|
85
+ val = counter.value
86
+ cmd << "INCRBY" << "u16" << "##{idx}" << val.to_s if val > 0
87
+ end
88
+
89
+ conn.bitfield(*cmd) if cmd.size > 3
90
+ conn.expire(key, HISTOGRAM_TTL)
91
+ key
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,136 @@
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
+ @lock.synchronize {
35
+ @grams[klass].record_time(time_ms)
36
+ @jobs["#{klass}|ms"] += time_ms
37
+ @totals["ms"] += time_ms
38
+ }
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 reset
104
+ @lock.synchronize {
105
+ array = [@totals, @jobs, @grams]
106
+ @totals = Hash.new(0)
107
+ @jobs = Hash.new(0)
108
+ @grams = Hash.new { |hash, key| hash[key] = Histogram.new(key) }
109
+ array
110
+ }
111
+ end
112
+ end
113
+
114
+ class Middleware
115
+ include Sidekiq::ServerMiddleware
116
+
117
+ def initialize(options)
118
+ @exec = options
119
+ end
120
+
121
+ def call(_instance, hash, queue, &block)
122
+ @exec.track(queue, hash["wrapped"] || hash["class"], &block)
123
+ end
124
+ end
125
+ end
126
+ end
127
+
128
+ Sidekiq.configure_server do |config|
129
+ exec = Sidekiq::Metrics::ExecutionTracker.new(config)
130
+ config.server_middleware do |chain|
131
+ chain.add Sidekiq::Metrics::Middleware, exec
132
+ end
133
+ config.on(:beat) do
134
+ exec.flush
135
+ end
136
+ end
@@ -1,82 +1,93 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "sidekiq/middleware/modules"
4
+
3
5
  module Sidekiq
4
6
  # Middleware is code configured to run before/after
5
- # a message is processed. It is patterned after Rack
7
+ # a job is processed. It is patterned after Rack
6
8
  # middleware. Middleware exists for the client side
7
9
  # (pushing jobs onto the queue) as well as the server
8
10
  # side (when jobs are actually processed).
9
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
+ #
10
17
  # To add middleware for the client:
11
18
  #
12
- # Sidekiq.configure_client do |config|
13
- # config.client_middleware do |chain|
14
- # chain.add MyClientHook
19
+ # Sidekiq.configure_client do |config|
20
+ # config.client_middleware do |chain|
21
+ # chain.add MyClientHook
22
+ # end
15
23
  # end
16
- # end
17
24
  #
18
25
  # To modify middleware for the server, just call
19
26
  # with another block:
20
27
  #
21
- # Sidekiq.configure_server do |config|
22
- # config.server_middleware do |chain|
23
- # chain.add MyServerHook
24
- # 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
25
33
  # end
26
- # end
27
34
  #
28
35
  # To insert immediately preceding another entry:
29
36
  #
30
- # Sidekiq.configure_client do |config|
31
- # config.client_middleware do |chain|
32
- # 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
33
41
  # end
34
- # end
35
42
  #
36
43
  # To insert immediately after another entry:
37
44
  #
38
- # Sidekiq.configure_client do |config|
39
- # config.client_middleware do |chain|
40
- # 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
41
49
  # end
42
- # end
43
50
  #
44
51
  # This is an example of a minimal server middleware:
45
52
  #
46
- # class MyServerHook
47
- # def call(worker_instance, msg, queue)
48
- # puts "Before work"
49
- # yield
50
- # 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
51
62
  # end
52
- # end
53
63
  #
54
64
  # This is an example of a minimal client middleware, note
55
65
  # the method must return the result or the job will not push
56
66
  # to Redis:
57
67
  #
58
- # class MyClientHook
59
- # def call(worker_class, msg, queue, redis_pool)
60
- # puts "Before push"
61
- # result = yield
62
- # puts "After push"
63
- # 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
64
77
  # end
65
- # end
66
78
  #
67
79
  module Middleware
68
80
  class Chain
69
81
  include Enumerable
70
82
 
71
- def initialize_copy(copy)
72
- copy.instance_variable_set(:@entries, entries.dup)
73
- end
74
-
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
88
+ # @api private
89
+ def initialize(config = nil) # :nodoc:
90
+ @config = config
80
91
  @entries = nil
81
92
  yield self if block_given?
82
93
  end
@@ -85,38 +96,62 @@ module Sidekiq
85
96
  @entries ||= []
86
97
  end
87
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]
88
107
  def remove(klass)
89
108
  entries.delete_if { |entry| entry.klass == klass }
90
109
  end
91
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
92
119
  def add(klass, *args)
93
120
  remove(klass)
94
- entries << Entry.new(klass, *args)
121
+ entries << Entry.new(@config, klass, *args)
95
122
  end
96
123
 
124
+ # Identical to {#add} except the middleware is added to the front of the chain.
97
125
  def prepend(klass, *args)
98
126
  remove(klass)
99
- entries.insert(0, Entry.new(klass, *args))
127
+ entries.insert(0, Entry.new(@config, klass, *args))
100
128
  end
101
129
 
130
+ # Inserts +newklass+ before +oldklass+ in the chain.
131
+ # Useful if one middleware must run before another middleware.
102
132
  def insert_before(oldklass, newklass, *args)
103
133
  i = entries.index { |entry| entry.klass == newklass }
104
- 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)
105
135
  i = entries.index { |entry| entry.klass == oldklass } || 0
106
136
  entries.insert(i, new_entry)
107
137
  end
108
138
 
139
+ # Inserts +newklass+ after +oldklass+ in the chain.
140
+ # Useful if one middleware must run after another middleware.
109
141
  def insert_after(oldklass, newklass, *args)
110
142
  i = entries.index { |entry| entry.klass == newklass }
111
- 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)
112
144
  i = entries.index { |entry| entry.klass == oldklass } || entries.count - 1
113
145
  entries.insert(i + 1, new_entry)
114
146
  end
115
147
 
148
+ # @return [Boolean] if the given class is already in the chain
116
149
  def exists?(klass)
117
150
  any? { |entry| entry.klass == klass }
118
151
  end
152
+ alias_method :include?, :exists?
119
153
 
154
+ # @return [Boolean] if the chain contains no middleware
120
155
  def empty?
121
156
  @entries.nil? || @entries.empty?
122
157
  end
@@ -129,33 +164,43 @@ module Sidekiq
129
164
  entries.clear
130
165
  end
131
166
 
132
- def invoke(*args)
167
+ # Used by Sidekiq to execute the middleware at runtime
168
+ # @api private
169
+ def invoke(*args, &block)
133
170
  return yield if empty?
134
171
 
135
172
  chain = retrieve
136
- traverse_chain = proc do
137
- if chain.empty?
138
- yield
139
- else
140
- 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)
141
184
  end
142
185
  end
143
- traverse_chain.call
144
186
  end
145
187
  end
146
188
 
147
- private
148
-
189
+ # Represents each link in the middleware chain
190
+ # @api private
149
191
  class Entry
150
192
  attr_reader :klass
151
193
 
152
- def initialize(klass, *args)
194
+ def initialize(config, klass, *args)
195
+ @config = config
153
196
  @klass = klass
154
197
  @args = args
155
198
  end
156
199
 
157
200
  def make_new
158
- @klass.new(*@args)
201
+ x = @klass.new(*@args)
202
+ x.config = @config if @config && x.respond_to?(:config=)
203
+ x
159
204
  end
160
205
  end
161
206
  end