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