sidekiq 5.2.7 → 7.0.2

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of sidekiq might be problematic. Click here for more details.

Files changed (146) hide show
  1. checksums.yaml +4 -4
  2. data/Changes.md +465 -5
  3. data/LICENSE.txt +9 -0
  4. data/README.md +32 -42
  5. data/bin/sidekiq +22 -3
  6. data/bin/sidekiqload +80 -77
  7. data/bin/sidekiqmon +8 -0
  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 +506 -351
  13. data/lib/sidekiq/capsule.rb +110 -0
  14. data/lib/sidekiq/cli.rb +202 -226
  15. data/lib/sidekiq/client.rb +104 -95
  16. data/lib/sidekiq/component.rb +66 -0
  17. data/lib/sidekiq/config.rb +270 -0
  18. data/lib/sidekiq/deploy.rb +62 -0
  19. data/lib/sidekiq/embedded.rb +61 -0
  20. data/lib/sidekiq/fetch.rb +49 -40
  21. data/lib/sidekiq/job.rb +378 -0
  22. data/lib/sidekiq/job_logger.rb +33 -7
  23. data/lib/sidekiq/job_retry.rb +127 -107
  24. data/lib/sidekiq/job_util.rb +71 -0
  25. data/lib/sidekiq/launcher.rb +197 -103
  26. data/lib/sidekiq/logger.rb +131 -0
  27. data/lib/sidekiq/manager.rb +43 -46
  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 +134 -0
  31. data/lib/sidekiq/middleware/chain.rb +104 -50
  32. data/lib/sidekiq/middleware/current_attributes.rb +58 -0
  33. data/lib/sidekiq/middleware/i18n.rb +7 -7
  34. data/lib/sidekiq/middleware/modules.rb +21 -0
  35. data/lib/sidekiq/monitor.rb +148 -0
  36. data/lib/sidekiq/paginator.rb +28 -16
  37. data/lib/sidekiq/processor.rb +105 -107
  38. data/lib/sidekiq/rails.rb +41 -37
  39. data/lib/sidekiq/redis_client_adapter.rb +115 -0
  40. data/lib/sidekiq/redis_connection.rb +38 -107
  41. data/lib/sidekiq/ring_buffer.rb +29 -0
  42. data/lib/sidekiq/scheduled.rb +111 -49
  43. data/lib/sidekiq/sd_notify.rb +149 -0
  44. data/lib/sidekiq/systemd.rb +24 -0
  45. data/lib/sidekiq/testing/inline.rb +6 -5
  46. data/lib/sidekiq/testing.rb +66 -84
  47. data/lib/sidekiq/transaction_aware_client.rb +44 -0
  48. data/lib/sidekiq/version.rb +3 -1
  49. data/lib/sidekiq/web/action.rb +15 -11
  50. data/lib/sidekiq/web/application.rb +108 -79
  51. data/lib/sidekiq/web/csrf_protection.rb +180 -0
  52. data/lib/sidekiq/web/helpers.rb +128 -105
  53. data/lib/sidekiq/web/router.rb +23 -19
  54. data/lib/sidekiq/web.rb +56 -107
  55. data/lib/sidekiq/worker_compatibility_alias.rb +13 -0
  56. data/lib/sidekiq.rb +92 -182
  57. data/sidekiq.gemspec +45 -16
  58. data/web/assets/images/apple-touch-icon.png +0 -0
  59. data/web/assets/javascripts/application.js +130 -61
  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 +166 -0
  64. data/web/assets/javascripts/dashboard.js +36 -292
  65. data/web/assets/javascripts/metrics.js +236 -0
  66. data/web/assets/stylesheets/application-dark.css +147 -0
  67. data/web/assets/stylesheets/application-rtl.css +2 -95
  68. data/web/assets/stylesheets/application.css +102 -522
  69. data/web/locales/ar.yml +71 -65
  70. data/web/locales/cs.yml +62 -62
  71. data/web/locales/da.yml +52 -52
  72. data/web/locales/de.yml +65 -53
  73. data/web/locales/el.yml +43 -24
  74. data/web/locales/en.yml +84 -66
  75. data/web/locales/es.yml +70 -54
  76. data/web/locales/fa.yml +65 -65
  77. data/web/locales/fr.yml +69 -62
  78. data/web/locales/he.yml +65 -64
  79. data/web/locales/hi.yml +59 -59
  80. data/web/locales/it.yml +53 -53
  81. data/web/locales/ja.yml +73 -64
  82. data/web/locales/ko.yml +52 -52
  83. data/web/locales/lt.yml +83 -0
  84. data/web/locales/nb.yml +61 -61
  85. data/web/locales/nl.yml +52 -52
  86. data/web/locales/pl.yml +45 -45
  87. data/web/locales/pt-br.yml +63 -55
  88. data/web/locales/pt.yml +51 -51
  89. data/web/locales/ru.yml +68 -63
  90. data/web/locales/sv.yml +53 -53
  91. data/web/locales/ta.yml +60 -60
  92. data/web/locales/uk.yml +62 -61
  93. data/web/locales/ur.yml +64 -64
  94. data/web/locales/vi.yml +83 -0
  95. data/web/locales/zh-cn.yml +43 -16
  96. data/web/locales/zh-tw.yml +42 -8
  97. data/web/views/_footer.erb +6 -3
  98. data/web/views/_job_info.erb +19 -2
  99. data/web/views/_nav.erb +1 -1
  100. data/web/views/_poll_link.erb +3 -6
  101. data/web/views/_summary.erb +7 -7
  102. data/web/views/busy.erb +74 -22
  103. data/web/views/dashboard.erb +58 -18
  104. data/web/views/dead.erb +3 -3
  105. data/web/views/layout.erb +3 -1
  106. data/web/views/metrics.erb +80 -0
  107. data/web/views/metrics_for_job.erb +69 -0
  108. data/web/views/morgue.erb +10 -7
  109. data/web/views/queue.erb +23 -10
  110. data/web/views/queues.erb +10 -2
  111. data/web/views/retries.erb +12 -9
  112. data/web/views/retry.erb +3 -3
  113. data/web/views/scheduled.erb +6 -3
  114. metadata +84 -69
  115. data/.circleci/config.yml +0 -61
  116. data/.github/contributing.md +0 -32
  117. data/.github/issue_template.md +0 -11
  118. data/.gitignore +0 -15
  119. data/.travis.yml +0 -11
  120. data/3.0-Upgrade.md +0 -70
  121. data/4.0-Upgrade.md +0 -53
  122. data/5.0-Upgrade.md +0 -56
  123. data/COMM-LICENSE +0 -97
  124. data/Ent-Changes.md +0 -238
  125. data/Gemfile +0 -23
  126. data/LICENSE +0 -9
  127. data/Pro-2.0-Upgrade.md +0 -138
  128. data/Pro-3.0-Upgrade.md +0 -44
  129. data/Pro-4.0-Upgrade.md +0 -35
  130. data/Pro-Changes.md +0 -759
  131. data/Rakefile +0 -9
  132. data/bin/sidekiqctl +0 -20
  133. data/code_of_conduct.md +0 -50
  134. data/lib/generators/sidekiq/worker_generator.rb +0 -49
  135. data/lib/sidekiq/core_ext.rb +0 -1
  136. data/lib/sidekiq/ctl.rb +0 -221
  137. data/lib/sidekiq/delay.rb +0 -42
  138. data/lib/sidekiq/exception_handler.rb +0 -29
  139. data/lib/sidekiq/extensions/action_mailer.rb +0 -57
  140. data/lib/sidekiq/extensions/active_record.rb +0 -40
  141. data/lib/sidekiq/extensions/class_methods.rb +0 -40
  142. data/lib/sidekiq/extensions/generic_proxy.rb +0 -31
  143. data/lib/sidekiq/logging.rb +0 -122
  144. data/lib/sidekiq/middleware/server/active_record.rb +0 -23
  145. data/lib/sidekiq/util.rb +0 -66
  146. data/lib/sidekiq/worker.rb +0 -220
@@ -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)
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
+ Float::INFINITY # the "maybe your job is too long" bucket
33
+ ]
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
+ ]
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(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,134 @@
1
+ require "time"
2
+ require "sidekiq"
3
+ require "sidekiq/metrics/shared"
4
+
5
+ # This file contains the components which track execution metrics within Sidekiq.
6
+ module Sidekiq
7
+ module Metrics
8
+ class ExecutionTracker
9
+ include Sidekiq::Component
10
+
11
+ def initialize(config)
12
+ @config = config
13
+ @jobs = Hash.new(0)
14
+ @totals = Hash.new(0)
15
+ @grams = Hash.new { |hash, key| hash[key] = Histogram.new(key) }
16
+ @lock = Mutex.new
17
+ end
18
+
19
+ def track(queue, klass)
20
+ start = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC, :millisecond)
21
+ time_ms = 0
22
+ begin
23
+ begin
24
+ yield
25
+ ensure
26
+ finish = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC, :millisecond)
27
+ time_ms = finish - start
28
+ end
29
+ # We don't track time for failed jobs as they can have very unpredictable
30
+ # execution times. more important to know average time for successful jobs so we
31
+ # 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
+ }
37
+ rescue Exception
38
+ @lock.synchronize {
39
+ @jobs["#{klass}|f"] += 1
40
+ @totals["f"] += 1
41
+ }
42
+ raise
43
+ ensure
44
+ @lock.synchronize {
45
+ @jobs["#{klass}|p"] += 1
46
+ @totals["p"] += 1
47
+ }
48
+ end
49
+ end
50
+
51
+ # LONG_TERM = 90 * 24 * 60 * 60
52
+ # MID_TERM = 7 * 24 * 60 * 60
53
+ SHORT_TERM = 8 * 60 * 60
54
+
55
+ def flush(time = Time.now)
56
+ totals, jobs, grams = reset
57
+ procd = totals["p"]
58
+ fails = totals["f"]
59
+ return if procd == 0 && fails == 0
60
+
61
+ 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")
65
+ count = 0
66
+
67
+ redis do |conn|
68
+ # persist fine-grained histogram data
69
+ if grams.size > 0
70
+ conn.pipelined do |pipe|
71
+ grams.each do |_, gram|
72
+ gram.persist(pipe, now)
73
+ end
74
+ end
75
+ end
76
+
77
+ # persist coarse grained execution count + execution millis.
78
+ # note as of today we don't use or do anything with the
79
+ # daily or hourly rollups.
80
+ [
81
+ # ["j", jobs, nowdate, LONG_TERM],
82
+ # ["j", jobs, nowhour, MID_TERM],
83
+ ["j", jobs, nowmin, SHORT_TERM]
84
+ ].each do |prefix, data, bucket, ttl|
85
+ conn.pipelined do |xa|
86
+ stats = "#{prefix}|#{bucket}"
87
+ data.each_pair do |key, value|
88
+ xa.hincrby stats, key, value
89
+ count += 1
90
+ end
91
+ xa.expire(stats, ttl)
92
+ end
93
+ end
94
+ logger.debug "Flushed #{count} metrics"
95
+ count
96
+ end
97
+ end
98
+
99
+ private
100
+
101
+ def reset
102
+ @lock.synchronize {
103
+ array = [@totals, @jobs, @grams]
104
+ @totals = Hash.new(0)
105
+ @jobs = Hash.new(0)
106
+ @grams = Hash.new { |hash, key| hash[key] = Histogram.new(key) }
107
+ array
108
+ }
109
+ end
110
+ end
111
+
112
+ class Middleware
113
+ include Sidekiq::ServerMiddleware
114
+
115
+ def initialize(options)
116
+ @exec = options
117
+ end
118
+
119
+ def call(_instance, hash, queue, &block)
120
+ @exec.track(queue, hash["wrapped"] || hash["class"], &block)
121
+ end
122
+ end
123
+ end
124
+ end
125
+
126
+ Sidekiq.configure_server do |config|
127
+ exec = Sidekiq::Metrics::ExecutionTracker.new(config)
128
+ config.server_middleware do |chain|
129
+ chain.add Sidekiq::Metrics::Middleware, exec
130
+ end
131
+ config.on(:beat) do
132
+ exec.flush
133
+ end
134
+ 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,9 +164,13 @@ module Sidekiq
121
164
  entries.clear
122
165
  end
123
166
 
167
+ # Used by Sidekiq to execute the middleware at runtime
168
+ # @api private
124
169
  def invoke(*args)
125
- chain = retrieve.dup
126
- traverse_chain = lambda do
170
+ return yield if empty?
171
+
172
+ chain = retrieve
173
+ traverse_chain = proc do
127
174
  if chain.empty?
128
175
  yield
129
176
  else
@@ -134,16 +181,23 @@ module Sidekiq
134
181
  end
135
182
  end
136
183
 
184
+ private
185
+
186
+ # Represents each link in the middleware chain
187
+ # @api private
137
188
  class Entry
138
189
  attr_reader :klass
139
190
 
140
- def initialize(klass, *args)
191
+ def initialize(config, klass, *args)
192
+ @config = config
141
193
  @klass = klass
142
- @args = args
194
+ @args = args
143
195
  end
144
196
 
145
197
  def make_new
146
- @klass.new(*@args)
198
+ x = @klass.new(*@args)
199
+ x.config = @config if @config && x.respond_to?(:config=)
200
+ x
147
201
  end
148
202
  end
149
203
  end
@@ -0,0 +1,58 @@
1
+ require "active_support/current_attributes"
2
+
3
+ module Sidekiq
4
+ ##
5
+ # Automatically save and load any current attributes in the execution context
6
+ # so context attributes "flow" from Rails actions into any associated jobs.
7
+ # This can be useful for multi-tenancy, i18n locale, timezone, any implicit
8
+ # per-request attribute. See +ActiveSupport::CurrentAttributes+.
9
+ #
10
+ # @example
11
+ #
12
+ # # in your initializer
13
+ # require "sidekiq/middleware/current_attributes"
14
+ # Sidekiq::CurrentAttributes.persist("Myapp::Current")
15
+ #
16
+ module CurrentAttributes
17
+ class Save
18
+ include Sidekiq::ClientMiddleware
19
+
20
+ def initialize(cattr)
21
+ @strklass = cattr
22
+ end
23
+
24
+ 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
31
+ end
32
+ end
33
+ yield
34
+ end
35
+ end
36
+
37
+ class Load
38
+ include Sidekiq::ServerMiddleware
39
+
40
+ def initialize(cattr)
41
+ @strklass = cattr
42
+ end
43
+
44
+ def call(_, job, _, &block)
45
+ if job.has_key?("cattr")
46
+ @strklass.constantize.set(job["cattr"], &block)
47
+ else
48
+ yield
49
+ end
50
+ end
51
+ end
52
+
53
+ def self.persist(klass, config = Sidekiq.default_configuration)
54
+ config.client_middleware.add Save, klass.to_s
55
+ config.server_middleware.add Load, klass.to_s
56
+ end
57
+ end
58
+ end