sidekiq 6.0.7 → 6.5.0

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 (107) hide show
  1. checksums.yaml +4 -4
  2. data/Changes.md +209 -2
  3. data/LICENSE +3 -3
  4. data/README.md +11 -10
  5. data/bin/sidekiq +8 -3
  6. data/bin/sidekiqload +70 -66
  7. data/bin/sidekiqmon +1 -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 +180 -123
  13. data/lib/sidekiq/cli.rb +80 -45
  14. data/lib/sidekiq/client.rb +52 -71
  15. data/lib/sidekiq/{util.rb → component.rb} +11 -14
  16. data/lib/sidekiq/delay.rb +2 -0
  17. data/lib/sidekiq/extensions/action_mailer.rb +3 -2
  18. data/lib/sidekiq/extensions/active_record.rb +4 -3
  19. data/lib/sidekiq/extensions/class_methods.rb +5 -4
  20. data/lib/sidekiq/extensions/generic_proxy.rb +4 -2
  21. data/lib/sidekiq/fetch.rb +41 -30
  22. data/lib/sidekiq/job.rb +13 -0
  23. data/lib/sidekiq/job_logger.rb +16 -28
  24. data/lib/sidekiq/job_retry.rb +36 -36
  25. data/lib/sidekiq/job_util.rb +71 -0
  26. data/lib/sidekiq/launcher.rb +123 -63
  27. data/lib/sidekiq/logger.rb +11 -20
  28. data/lib/sidekiq/manager.rb +35 -34
  29. data/lib/sidekiq/middleware/chain.rb +28 -17
  30. data/lib/sidekiq/middleware/current_attributes.rb +61 -0
  31. data/lib/sidekiq/middleware/i18n.rb +6 -4
  32. data/lib/sidekiq/middleware/modules.rb +19 -0
  33. data/lib/sidekiq/monitor.rb +1 -1
  34. data/lib/sidekiq/paginator.rb +8 -8
  35. data/lib/sidekiq/processor.rb +41 -41
  36. data/lib/sidekiq/rails.rb +38 -22
  37. data/lib/sidekiq/redis_client_adapter.rb +154 -0
  38. data/lib/sidekiq/redis_connection.rb +87 -53
  39. data/lib/sidekiq/ring_buffer.rb +29 -0
  40. data/lib/sidekiq/scheduled.rb +60 -24
  41. data/lib/sidekiq/sd_notify.rb +1 -1
  42. data/lib/sidekiq/testing/inline.rb +4 -4
  43. data/lib/sidekiq/testing.rb +39 -40
  44. data/lib/sidekiq/transaction_aware_client.rb +45 -0
  45. data/lib/sidekiq/version.rb +1 -1
  46. data/lib/sidekiq/web/action.rb +2 -2
  47. data/lib/sidekiq/web/application.rb +21 -12
  48. data/lib/sidekiq/web/csrf_protection.rb +180 -0
  49. data/lib/sidekiq/web/helpers.rb +40 -34
  50. data/lib/sidekiq/web/router.rb +5 -2
  51. data/lib/sidekiq/web.rb +36 -72
  52. data/lib/sidekiq/worker.rb +136 -16
  53. data/lib/sidekiq.rb +107 -30
  54. data/sidekiq.gemspec +11 -4
  55. data/web/assets/images/apple-touch-icon.png +0 -0
  56. data/web/assets/javascripts/application.js +113 -65
  57. data/web/assets/javascripts/dashboard.js +51 -51
  58. data/web/assets/stylesheets/application-dark.css +64 -43
  59. data/web/assets/stylesheets/application-rtl.css +0 -4
  60. data/web/assets/stylesheets/application.css +42 -239
  61. data/web/locales/ar.yml +8 -2
  62. data/web/locales/en.yml +4 -1
  63. data/web/locales/es.yml +18 -2
  64. data/web/locales/fr.yml +8 -1
  65. data/web/locales/ja.yml +3 -0
  66. data/web/locales/lt.yml +1 -1
  67. data/web/locales/pl.yml +4 -4
  68. data/web/locales/pt-br.yml +27 -9
  69. data/web/locales/ru.yml +4 -0
  70. data/web/views/_footer.erb +1 -1
  71. data/web/views/_job_info.erb +1 -1
  72. data/web/views/_poll_link.erb +2 -5
  73. data/web/views/_summary.erb +7 -7
  74. data/web/views/busy.erb +51 -20
  75. data/web/views/dashboard.erb +22 -14
  76. data/web/views/dead.erb +1 -1
  77. data/web/views/layout.erb +2 -1
  78. data/web/views/morgue.erb +6 -6
  79. data/web/views/queue.erb +11 -11
  80. data/web/views/queues.erb +4 -4
  81. data/web/views/retries.erb +7 -7
  82. data/web/views/retry.erb +1 -1
  83. data/web/views/scheduled.erb +1 -1
  84. metadata +29 -51
  85. data/.circleci/config.yml +0 -60
  86. data/.github/contributing.md +0 -32
  87. data/.github/issue_template.md +0 -11
  88. data/.gitignore +0 -13
  89. data/.standard.yml +0 -20
  90. data/3.0-Upgrade.md +0 -70
  91. data/4.0-Upgrade.md +0 -53
  92. data/5.0-Upgrade.md +0 -56
  93. data/6.0-Upgrade.md +0 -72
  94. data/COMM-LICENSE +0 -97
  95. data/Ent-2.0-Upgrade.md +0 -37
  96. data/Ent-Changes.md +0 -256
  97. data/Gemfile +0 -24
  98. data/Gemfile.lock +0 -208
  99. data/Pro-2.0-Upgrade.md +0 -138
  100. data/Pro-3.0-Upgrade.md +0 -44
  101. data/Pro-4.0-Upgrade.md +0 -35
  102. data/Pro-5.0-Upgrade.md +0 -25
  103. data/Pro-Changes.md +0 -782
  104. data/Rakefile +0 -10
  105. data/code_of_conduct.md +0 -50
  106. data/lib/generators/sidekiq/worker_generator.rb +0 -57
  107. data/lib/sidekiq/exception_handler.rb +0 -27
@@ -3,11 +3,12 @@
3
3
  require "sidekiq/manager"
4
4
  require "sidekiq/fetch"
5
5
  require "sidekiq/scheduled"
6
+ require "sidekiq/ring_buffer"
6
7
 
7
8
  module Sidekiq
8
9
  # The Launcher starts the Manager and Poller threads and provides the process heartbeat.
9
10
  class Launcher
10
- include Util
11
+ include Sidekiq::Component
11
12
 
12
13
  STATS_TTL = 5 * 365 * 24 * 60 * 60 # 5 years
13
14
 
@@ -15,17 +16,18 @@ module Sidekiq
15
16
  proc { "sidekiq" },
16
17
  proc { Sidekiq::VERSION },
17
18
  proc { |me, data| data["tag"] },
18
- proc { |me, data| "[#{Processor::WORKER_STATE.size} of #{data["concurrency"]} busy]" },
19
+ proc { |me, data| "[#{Processor::WORK_STATE.size} of #{data["concurrency"]} busy]" },
19
20
  proc { |me, data| "stopping" if me.stopping? }
20
21
  ]
21
22
 
22
23
  attr_accessor :manager, :poller, :fetcher
23
24
 
24
25
  def initialize(options)
26
+ @config = options
27
+ options[:fetch] ||= BasicFetch.new(options)
25
28
  @manager = Sidekiq::Manager.new(options)
26
- @poller = Sidekiq::Scheduled::Poller.new
29
+ @poller = Sidekiq::Scheduled::Poller.new(options)
27
30
  @done = false
28
- @options = options
29
31
  end
30
32
 
31
33
  def run
@@ -42,11 +44,9 @@ module Sidekiq
42
44
  @poller.terminate
43
45
  end
44
46
 
45
- # Shuts down the process. This method does not
46
- # return until all work is complete and cleaned up.
47
- # It can take up to the timeout to complete.
47
+ # Shuts down this Sidekiq instance. Waits up to the deadline for all jobs to complete.
48
48
  def stop
49
- deadline = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC) + @options[:timeout]
49
+ deadline = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC) + @config[:timeout]
50
50
 
51
51
  @done = true
52
52
  @manager.quiet
@@ -54,10 +54,10 @@ module Sidekiq
54
54
 
55
55
  @manager.stop(deadline)
56
56
 
57
- # Requeue everything in case there was a worker who grabbed work while stopped
57
+ # Requeue everything in case there was a thread which fetched a job while the process was stopped.
58
58
  # This call is a no-op in Sidekiq but necessary for Sidekiq Pro.
59
- strategy = (@options[:fetch] || Sidekiq::BasicFetch)
60
- strategy.bulk_requeue([], @options)
59
+ strategy = @config[:fetch]
60
+ strategy.bulk_requeue([], @config)
61
61
 
62
62
  clear_heartbeat
63
63
  end
@@ -68,22 +68,24 @@ module Sidekiq
68
68
 
69
69
  private unless $TESTING
70
70
 
71
+ BEAT_PAUSE = 5
72
+
71
73
  def start_heartbeat
72
74
  loop do
73
75
  heartbeat
74
- sleep 5
76
+ sleep BEAT_PAUSE
75
77
  end
76
- Sidekiq.logger.info("Heartbeat stopping...")
78
+ logger.info("Heartbeat stopping...")
77
79
  end
78
80
 
79
81
  def clear_heartbeat
80
82
  # Remove record from Redis since we are shutting down.
81
83
  # Note we don't stop the heartbeat thread; if the process
82
84
  # doesn't actually exit, it'll reappear in the Web UI.
83
- Sidekiq.redis do |conn|
84
- conn.pipelined do
85
- conn.srem("processes", identity)
86
- conn.unlink("#{identity}:workers")
85
+ redis do |conn|
86
+ conn.pipelined do |pipeline|
87
+ pipeline.srem("processes", identity)
88
+ pipeline.unlink("#{identity}:work")
87
89
  end
88
90
  end
89
91
  rescue
@@ -104,14 +106,14 @@ module Sidekiq
104
106
  nowdate = Time.now.utc.strftime("%Y-%m-%d")
105
107
  begin
106
108
  Sidekiq.redis do |conn|
107
- conn.pipelined do
108
- conn.incrby("stat:processed", procd)
109
- conn.incrby("stat:processed:#{nowdate}", procd)
110
- conn.expire("stat:processed:#{nowdate}", STATS_TTL)
111
-
112
- conn.incrby("stat:failed", fails)
113
- conn.incrby("stat:failed:#{nowdate}", fails)
114
- conn.expire("stat:failed:#{nowdate}", STATS_TTL)
109
+ conn.pipelined do |pipeline|
110
+ pipeline.incrby("stat:processed", procd)
111
+ pipeline.incrby("stat:processed:#{nowdate}", procd)
112
+ pipeline.expire("stat:processed:#{nowdate}", STATS_TTL)
113
+
114
+ pipeline.incrby("stat:failed", fails)
115
+ pipeline.incrby("stat:failed:#{nowdate}", fails)
116
+ pipeline.expire("stat:failed:#{nowdate}", STATS_TTL)
115
117
  end
116
118
  end
117
119
  rescue => ex
@@ -129,38 +131,49 @@ module Sidekiq
129
131
  begin
130
132
  fails = Processor::FAILURE.reset
131
133
  procd = Processor::PROCESSED.reset
132
- curstate = Processor::WORKER_STATE.dup
134
+ curstate = Processor::WORK_STATE.dup
133
135
 
134
- workers_key = "#{key}:workers"
135
136
  nowdate = Time.now.utc.strftime("%Y-%m-%d")
136
137
 
137
- Sidekiq.redis do |conn|
138
- conn.multi do
139
- conn.incrby("stat:processed", procd)
140
- conn.incrby("stat:processed:#{nowdate}", procd)
141
- conn.expire("stat:processed:#{nowdate}", STATS_TTL)
138
+ redis do |conn|
139
+ conn.multi do |transaction|
140
+ transaction.incrby("stat:processed", procd)
141
+ transaction.incrby("stat:processed:#{nowdate}", procd)
142
+ transaction.expire("stat:processed:#{nowdate}", STATS_TTL)
142
143
 
143
- conn.incrby("stat:failed", fails)
144
- conn.incrby("stat:failed:#{nowdate}", fails)
145
- conn.expire("stat:failed:#{nowdate}", STATS_TTL)
144
+ transaction.incrby("stat:failed", fails)
145
+ transaction.incrby("stat:failed:#{nowdate}", fails)
146
+ transaction.expire("stat:failed:#{nowdate}", STATS_TTL)
147
+ end
146
148
 
147
- conn.unlink(workers_key)
149
+ # work is the current set of executing jobs
150
+ work_key = "#{key}:work"
151
+ conn.pipelined do |transaction|
152
+ transaction.unlink(work_key)
148
153
  curstate.each_pair do |tid, hash|
149
- conn.hset(workers_key, tid, Sidekiq.dump_json(hash))
154
+ transaction.hset(work_key, tid, Sidekiq.dump_json(hash))
150
155
  end
151
- conn.expire(workers_key, 60)
156
+ transaction.expire(work_key, 60)
152
157
  end
153
158
  end
154
159
 
155
- fails = procd = 0
160
+ rtt = check_rtt
156
161
 
157
- _, exists, _, _, msg = Sidekiq.redis { |conn|
158
- conn.multi {
159
- conn.sadd("processes", key)
160
- conn.exists(key)
161
- conn.hmset(key, "info", to_json, "busy", curstate.size, "beat", Time.now.to_f, "quiet", @done)
162
- conn.expire(key, 60)
163
- conn.rpop("#{key}-signals")
162
+ fails = procd = 0
163
+ kb = memory_usage(::Process.pid)
164
+
165
+ _, exists, _, _, msg = redis { |conn|
166
+ conn.multi { |transaction|
167
+ transaction.sadd("processes", key)
168
+ transaction.exists?(key)
169
+ transaction.hmset(key, "info", to_json,
170
+ "busy", curstate.size,
171
+ "beat", Time.now.to_f,
172
+ "rtt_us", rtt,
173
+ "quiet", @done.to_s,
174
+ "rss", kb)
175
+ transaction.expire(key, 60)
176
+ transaction.rpop("#{key}-signals")
164
177
  }
165
178
  }
166
179
 
@@ -179,27 +192,74 @@ module Sidekiq
179
192
  end
180
193
  end
181
194
 
182
- def to_data
183
- @data ||= begin
184
- {
185
- "hostname" => hostname,
186
- "started_at" => Time.now.to_f,
187
- "pid" => ::Process.pid,
188
- "tag" => @options[:tag] || "",
189
- "concurrency" => @options[:concurrency],
190
- "queues" => @options[:queues].uniq,
191
- "labels" => @options[:labels],
192
- "identity" => identity
193
- }
195
+ # We run the heartbeat every five seconds.
196
+ # Capture five samples of RTT, log a warning if each sample
197
+ # is above our warning threshold.
198
+ RTT_READINGS = RingBuffer.new(5)
199
+ RTT_WARNING_LEVEL = 50_000
200
+
201
+ def check_rtt
202
+ a = b = 0
203
+ redis do |x|
204
+ a = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC, :microsecond)
205
+ x.ping
206
+ b = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC, :microsecond)
194
207
  end
208
+ rtt = b - a
209
+ RTT_READINGS << rtt
210
+ # Ideal RTT for Redis is < 1000µs
211
+ # Workable is < 10,000µs
212
+ # Log a warning if it's a disaster.
213
+ if RTT_READINGS.all? { |x| x > RTT_WARNING_LEVEL }
214
+ logger.warn <<~EOM
215
+ Your Redis network connection is performing extremely poorly.
216
+ Last RTT readings were #{RTT_READINGS.buffer.inspect}, ideally these should be < 1000.
217
+ Ensure Redis is running in the same AZ or datacenter as Sidekiq.
218
+ If these values are close to 100,000, that means your Sidekiq process may be
219
+ CPU-saturated; reduce your concurrency and/or see https://github.com/mperham/sidekiq/discussions/5039
220
+ EOM
221
+ RTT_READINGS.reset
222
+ end
223
+ rtt
224
+ end
225
+
226
+ MEMORY_GRABBER = case RUBY_PLATFORM
227
+ when /linux/
228
+ ->(pid) {
229
+ IO.readlines("/proc/#{$$}/status").each do |line|
230
+ next unless line.start_with?("VmRSS:")
231
+ break line.split[1].to_i
232
+ end
233
+ }
234
+ when /darwin|bsd/
235
+ ->(pid) {
236
+ `ps -o pid,rss -p #{pid}`.lines.last.split.last.to_i
237
+ }
238
+ else
239
+ ->(pid) { 0 }
240
+ end
241
+
242
+ def memory_usage(pid)
243
+ MEMORY_GRABBER.call(pid)
244
+ end
245
+
246
+ def to_data
247
+ @data ||= {
248
+ "hostname" => hostname,
249
+ "started_at" => Time.now.to_f,
250
+ "pid" => ::Process.pid,
251
+ "tag" => @config[:tag] || "",
252
+ "concurrency" => @config[:concurrency],
253
+ "queues" => @config[:queues].uniq,
254
+ "labels" => @config[:labels],
255
+ "identity" => identity
256
+ }
195
257
  end
196
258
 
197
259
  def to_json
198
- @json ||= begin
199
- # this data changes infrequently so dump it to a string
200
- # now so we don't need to dump it every heartbeat.
201
- Sidekiq.dump_json(to_data)
202
- end
260
+ # this data changes infrequently so dump it to a string
261
+ # now so we don't need to dump it every heartbeat.
262
+ @json ||= Sidekiq.dump_json(to_data)
203
263
  end
204
264
  end
205
265
  end
@@ -6,15 +6,20 @@ require "time"
6
6
  module Sidekiq
7
7
  module Context
8
8
  def self.with(hash)
9
+ orig_context = current.dup
9
10
  current.merge!(hash)
10
11
  yield
11
12
  ensure
12
- hash.each_key { |key| current.delete(key) }
13
+ Thread.current[:sidekiq_context] = orig_context
13
14
  end
14
15
 
15
16
  def self.current
16
17
  Thread.current[:sidekiq_context] ||= {}
17
18
  end
19
+
20
+ def self.add(k, v)
21
+ current[k] = v
22
+ end
18
23
  end
19
24
 
20
25
  module LoggingUtils
@@ -30,24 +35,10 @@ module Sidekiq
30
35
  nil
31
36
  end
32
37
 
33
- def debug?
34
- level <= 0
35
- end
36
-
37
- def info?
38
- level <= 1
39
- end
40
-
41
- def warn?
42
- level <= 2
43
- end
44
-
45
- def error?
46
- level <= 3
47
- end
48
-
49
- def fatal?
50
- level <= 4
38
+ LEVELS.each do |level, numeric_level|
39
+ define_method("#{level}?") do
40
+ local_level.nil? ? super() : local_level <= numeric_level
41
+ end
51
42
  end
52
43
 
53
44
  def local_level
@@ -89,7 +80,7 @@ module Sidekiq
89
80
  return true if @logdev.nil? || severity < level
90
81
 
91
82
  if message.nil?
92
- if block_given?
83
+ if block
93
84
  message = yield
94
85
  else
95
86
  message = progname
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "sidekiq/util"
4
3
  require "sidekiq/processor"
5
4
  require "sidekiq/fetch"
6
5
  require "set"
@@ -21,43 +20,37 @@ module Sidekiq
21
20
  # the shutdown process. The other tasks are performed by other threads.
22
21
  #
23
22
  class Manager
24
- include Util
23
+ include Sidekiq::Component
25
24
 
26
25
  attr_reader :workers
27
- attr_reader :options
28
26
 
29
27
  def initialize(options = {})
28
+ @config = options
30
29
  logger.debug { options.inspect }
31
- @options = options
32
30
  @count = options[:concurrency] || 10
33
31
  raise ArgumentError, "Concurrency of #{@count} is not supported" if @count < 1
34
32
 
35
33
  @done = false
36
34
  @workers = Set.new
37
35
  @count.times do
38
- @workers << Processor.new(self)
36
+ @workers << Processor.new(@config, &method(:processor_result))
39
37
  end
40
38
  @plock = Mutex.new
41
39
  end
42
40
 
43
41
  def start
44
- @workers.each do |x|
45
- x.start
46
- end
42
+ @workers.each(&:start)
47
43
  end
48
44
 
49
45
  def quiet
50
46
  return if @done
51
47
  @done = true
52
48
 
53
- logger.info { "Terminating quiet workers" }
54
- @workers.each { |x| x.terminate }
49
+ logger.info { "Terminating quiet threads" }
50
+ @workers.each(&:terminate)
55
51
  fire_event(:quiet, reverse: true)
56
52
  end
57
53
 
58
- # hack for quicker development / testing environment #2774
59
- PAUSE_TIME = STDOUT.tty? ? 0.1 : 0.5
60
-
61
54
  def stop(deadline)
62
55
  quiet
63
56
  fire_event(:shutdown, reverse: true)
@@ -68,29 +61,18 @@ module Sidekiq
68
61
  sleep PAUSE_TIME
69
62
  return if @workers.empty?
70
63
 
71
- logger.info { "Pausing to allow workers to finish..." }
72
- remaining = deadline - ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
73
- while remaining > PAUSE_TIME
74
- return if @workers.empty?
75
- sleep PAUSE_TIME
76
- remaining = deadline - ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
77
- end
64
+ logger.info { "Pausing to allow jobs to finish..." }
65
+ wait_for(deadline) { @workers.empty? }
78
66
  return if @workers.empty?
79
67
 
80
68
  hard_shutdown
81
69
  end
82
70
 
83
- def processor_stopped(processor)
84
- @plock.synchronize do
85
- @workers.delete(processor)
86
- end
87
- end
88
-
89
- def processor_died(processor, reason)
71
+ def processor_result(processor, reason = nil)
90
72
  @plock.synchronize do
91
73
  @workers.delete(processor)
92
74
  unless @done
93
- p = Processor.new(self)
75
+ p = Processor.new(@config, &method(:processor_result))
94
76
  @workers << p
95
77
  p.start
96
78
  end
@@ -104,7 +86,7 @@ module Sidekiq
104
86
  private
105
87
 
106
88
  def hard_shutdown
107
- # We've reached the timeout and we still have busy workers.
89
+ # We've reached the timeout and we still have busy threads.
108
90
  # They must die but their jobs shall live on.
109
91
  cleanup = nil
110
92
  @plock.synchronize do
@@ -114,22 +96,41 @@ module Sidekiq
114
96
  if cleanup.size > 0
115
97
  jobs = cleanup.map { |p| p.job }.compact
116
98
 
117
- logger.warn { "Terminating #{cleanup.size} busy worker threads" }
118
- logger.warn { "Work still in progress #{jobs.inspect}" }
99
+ logger.warn { "Terminating #{cleanup.size} busy threads" }
100
+ logger.debug { "Jobs still in progress #{jobs.inspect}" }
119
101
 
120
102
  # Re-enqueue unfinished jobs
121
103
  # NOTE: You may notice that we may push a job back to redis before
122
- # the worker thread is terminated. This is ok because Sidekiq's
104
+ # the thread is terminated. This is ok because Sidekiq's
123
105
  # contract says that jobs are run AT LEAST once. Process termination
124
106
  # is delayed until we're certain the jobs are back in Redis because
125
107
  # it is worse to lose a job than to run it twice.
126
- strategy = (@options[:fetch] || Sidekiq::BasicFetch)
127
- strategy.bulk_requeue(jobs, @options)
108
+ strategy = @config[:fetch]
109
+ strategy.bulk_requeue(jobs, @config)
128
110
  end
129
111
 
130
112
  cleanup.each do |processor|
131
113
  processor.kill
132
114
  end
115
+
116
+ # when this method returns, we immediately call `exit` which may not give
117
+ # the remaining threads time to run `ensure` blocks, etc. We pause here up
118
+ # to 3 seconds to give threads a minimal amount of time to run `ensure` blocks.
119
+ deadline = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC) + 3
120
+ wait_for(deadline) { @workers.empty? }
121
+ end
122
+
123
+ # hack for quicker development / testing environment #2774
124
+ PAUSE_TIME = $stdout.tty? ? 0.1 : 0.5
125
+
126
+ # Wait for the orblock to be true or the deadline passed.
127
+ def wait_for(deadline, &condblock)
128
+ remaining = deadline - ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
129
+ while remaining > PAUSE_TIME
130
+ return if condblock.call
131
+ sleep PAUSE_TIME
132
+ remaining = deadline - ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
133
+ end
133
134
  end
134
135
  end
135
136
  end
@@ -1,5 +1,7 @@
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
7
  # a message is processed. It is patterned after Rack
@@ -44,10 +46,12 @@ module Sidekiq
44
46
  # This is an example of a minimal server middleware:
45
47
  #
46
48
  # class MyServerHook
47
- # def call(worker_instance, msg, queue)
48
- # puts "Before work"
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
49
53
  # yield
50
- # puts "After work"
54
+ # logger.info "After job"
51
55
  # end
52
56
  # end
53
57
  #
@@ -56,10 +60,11 @@ module Sidekiq
56
60
  # to Redis:
57
61
  #
58
62
  # class MyClientHook
59
- # def call(worker_class, msg, queue, redis_pool)
60
- # puts "Before push"
63
+ # include Sidekiq::ClientMiddleware
64
+ # def call(job_class, msg, queue, redis_pool)
65
+ # logger.info "Before push"
61
66
  # result = yield
62
- # puts "After push"
67
+ # logger.info "After push"
63
68
  # result
64
69
  # end
65
70
  # end
@@ -76,7 +81,8 @@ module Sidekiq
76
81
  entries.each(&block)
77
82
  end
78
83
 
79
- def initialize
84
+ def initialize(config = nil)
85
+ @config = config
80
86
  @entries = nil
81
87
  yield self if block_given?
82
88
  end
@@ -90,25 +96,25 @@ module Sidekiq
90
96
  end
91
97
 
92
98
  def add(klass, *args)
93
- remove(klass) if exists?(klass)
94
- entries << Entry.new(klass, *args)
99
+ remove(klass)
100
+ entries << Entry.new(@config, klass, *args)
95
101
  end
96
102
 
97
103
  def prepend(klass, *args)
98
- remove(klass) if exists?(klass)
99
- entries.insert(0, Entry.new(klass, *args))
104
+ remove(klass)
105
+ entries.insert(0, Entry.new(@config, klass, *args))
100
106
  end
101
107
 
102
108
  def insert_before(oldklass, newklass, *args)
103
109
  i = entries.index { |entry| entry.klass == newklass }
104
- new_entry = i.nil? ? Entry.new(newklass, *args) : entries.delete_at(i)
110
+ new_entry = i.nil? ? Entry.new(@config, newklass, *args) : entries.delete_at(i)
105
111
  i = entries.index { |entry| entry.klass == oldklass } || 0
106
112
  entries.insert(i, new_entry)
107
113
  end
108
114
 
109
115
  def insert_after(oldklass, newklass, *args)
110
116
  i = entries.index { |entry| entry.klass == newklass }
111
- new_entry = i.nil? ? Entry.new(newklass, *args) : entries.delete_at(i)
117
+ new_entry = i.nil? ? Entry.new(@config, newklass, *args) : entries.delete_at(i)
112
118
  i = entries.index { |entry| entry.klass == oldklass } || entries.count - 1
113
119
  entries.insert(i + 1, new_entry)
114
120
  end
@@ -132,8 +138,8 @@ module Sidekiq
132
138
  def invoke(*args)
133
139
  return yield if empty?
134
140
 
135
- chain = retrieve.dup
136
- traverse_chain = lambda do
141
+ chain = retrieve
142
+ traverse_chain = proc do
137
143
  if chain.empty?
138
144
  yield
139
145
  else
@@ -144,16 +150,21 @@ module Sidekiq
144
150
  end
145
151
  end
146
152
 
153
+ private
154
+
147
155
  class Entry
148
156
  attr_reader :klass
149
157
 
150
- def initialize(klass, *args)
158
+ def initialize(config, klass, *args)
159
+ @config = config
151
160
  @klass = klass
152
161
  @args = args
153
162
  end
154
163
 
155
164
  def make_new
156
- @klass.new(*@args)
165
+ x = @klass.new(*@args)
166
+ x.config = @config if @config && x.respond_to?(:config=)
167
+ x
157
168
  end
158
169
  end
159
170
  end
@@ -0,0 +1,61 @@
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
+ @klass = cattr
22
+ end
23
+
24
+ def call(_, job, _, _)
25
+ attrs = @klass.attributes
26
+ if job.has_key?("cattr")
27
+ job["cattr"].merge!(attrs)
28
+ else
29
+ job["cattr"] = attrs
30
+ end
31
+ yield
32
+ end
33
+ end
34
+
35
+ class Load
36
+ include Sidekiq::ServerMiddleware
37
+
38
+ def initialize(cattr)
39
+ @klass = cattr
40
+ end
41
+
42
+ def call(_, job, _, &block)
43
+ if job.has_key?("cattr")
44
+ @klass.set(job["cattr"], &block)
45
+ else
46
+ yield
47
+ end
48
+ end
49
+ end
50
+
51
+ def self.persist(klass)
52
+ Sidekiq.configure_client do |config|
53
+ config.client_middleware.add Save, klass
54
+ end
55
+ Sidekiq.configure_server do |config|
56
+ config.client_middleware.add Save, klass
57
+ config.server_middleware.add Load, klass
58
+ end
59
+ end
60
+ end
61
+ end
@@ -10,16 +10,18 @@ module Sidekiq::Middleware::I18n
10
10
  # Get the current locale and store it in the message
11
11
  # to be sent to Sidekiq.
12
12
  class Client
13
- def call(_worker, msg, _queue, _redis)
14
- msg["locale"] ||= I18n.locale
13
+ include Sidekiq::ClientMiddleware
14
+ def call(_jobclass, job, _queue, _redis)
15
+ job["locale"] ||= I18n.locale
15
16
  yield
16
17
  end
17
18
  end
18
19
 
19
20
  # Pull the msg locale out and set the current thread to use it.
20
21
  class Server
21
- def call(_worker, msg, _queue, &block)
22
- I18n.with_locale(msg.fetch("locale", I18n.default_locale), &block)
22
+ include Sidekiq::ServerMiddleware
23
+ def call(_jobclass, job, _queue, &block)
24
+ I18n.with_locale(job.fetch("locale", I18n.default_locale), &block)
23
25
  end
24
26
  end
25
27
  end