sidekiq 5.2.9 → 6.4.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 (106) hide show
  1. checksums.yaml +4 -4
  2. data/Changes.md +311 -1
  3. data/LICENSE +3 -3
  4. data/README.md +21 -37
  5. data/bin/sidekiq +26 -2
  6. data/bin/sidekiqload +32 -24
  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 +313 -249
  13. data/lib/sidekiq/cli.rb +155 -181
  14. data/lib/sidekiq/client.rb +42 -60
  15. data/lib/sidekiq/delay.rb +7 -6
  16. data/lib/sidekiq/exception_handler.rb +10 -12
  17. data/lib/sidekiq/extensions/action_mailer.rb +15 -24
  18. data/lib/sidekiq/extensions/active_record.rb +15 -12
  19. data/lib/sidekiq/extensions/class_methods.rb +16 -13
  20. data/lib/sidekiq/extensions/generic_proxy.rb +8 -6
  21. data/lib/sidekiq/fetch.rb +39 -31
  22. data/lib/sidekiq/job.rb +13 -0
  23. data/lib/sidekiq/job_logger.rb +45 -7
  24. data/lib/sidekiq/job_retry.rb +70 -71
  25. data/lib/sidekiq/job_util.rb +65 -0
  26. data/lib/sidekiq/launcher.rb +150 -60
  27. data/lib/sidekiq/logger.rb +166 -0
  28. data/lib/sidekiq/manager.rb +17 -21
  29. data/lib/sidekiq/middleware/chain.rb +20 -8
  30. data/lib/sidekiq/middleware/current_attributes.rb +57 -0
  31. data/lib/sidekiq/middleware/i18n.rb +5 -7
  32. data/lib/sidekiq/monitor.rb +133 -0
  33. data/lib/sidekiq/paginator.rb +18 -14
  34. data/lib/sidekiq/processor.rb +71 -70
  35. data/lib/sidekiq/rails.rb +40 -37
  36. data/lib/sidekiq/redis_connection.rb +48 -48
  37. data/lib/sidekiq/scheduled.rb +62 -28
  38. data/lib/sidekiq/sd_notify.rb +149 -0
  39. data/lib/sidekiq/systemd.rb +24 -0
  40. data/lib/sidekiq/testing/inline.rb +2 -1
  41. data/lib/sidekiq/testing.rb +36 -27
  42. data/lib/sidekiq/util.rb +57 -15
  43. data/lib/sidekiq/version.rb +2 -1
  44. data/lib/sidekiq/web/action.rb +15 -11
  45. data/lib/sidekiq/web/application.rb +88 -75
  46. data/lib/sidekiq/web/csrf_protection.rb +180 -0
  47. data/lib/sidekiq/web/helpers.rb +109 -92
  48. data/lib/sidekiq/web/router.rb +23 -19
  49. data/lib/sidekiq/web.rb +61 -105
  50. data/lib/sidekiq/worker.rb +249 -105
  51. data/lib/sidekiq.rb +76 -44
  52. data/sidekiq.gemspec +23 -16
  53. data/web/assets/images/apple-touch-icon.png +0 -0
  54. data/web/assets/javascripts/application.js +83 -64
  55. data/web/assets/javascripts/dashboard.js +54 -73
  56. data/web/assets/stylesheets/application-dark.css +143 -0
  57. data/web/assets/stylesheets/application-rtl.css +0 -4
  58. data/web/assets/stylesheets/application.css +43 -232
  59. data/web/locales/ar.yml +8 -2
  60. data/web/locales/de.yml +14 -2
  61. data/web/locales/en.yml +6 -1
  62. data/web/locales/es.yml +18 -2
  63. data/web/locales/fr.yml +10 -3
  64. data/web/locales/ja.yml +7 -1
  65. data/web/locales/lt.yml +83 -0
  66. data/web/locales/pl.yml +4 -4
  67. data/web/locales/ru.yml +4 -0
  68. data/web/locales/vi.yml +83 -0
  69. data/web/views/_footer.erb +1 -1
  70. data/web/views/_job_info.erb +3 -2
  71. data/web/views/_poll_link.erb +2 -5
  72. data/web/views/_summary.erb +7 -7
  73. data/web/views/busy.erb +54 -20
  74. data/web/views/dashboard.erb +22 -14
  75. data/web/views/dead.erb +3 -3
  76. data/web/views/layout.erb +3 -1
  77. data/web/views/morgue.erb +9 -6
  78. data/web/views/queue.erb +19 -10
  79. data/web/views/queues.erb +10 -2
  80. data/web/views/retries.erb +11 -8
  81. data/web/views/retry.erb +3 -3
  82. data/web/views/scheduled.erb +5 -2
  83. metadata +34 -64
  84. data/.circleci/config.yml +0 -61
  85. data/.github/contributing.md +0 -32
  86. data/.github/issue_template.md +0 -11
  87. data/.gitignore +0 -15
  88. data/.travis.yml +0 -11
  89. data/3.0-Upgrade.md +0 -70
  90. data/4.0-Upgrade.md +0 -53
  91. data/5.0-Upgrade.md +0 -56
  92. data/COMM-LICENSE +0 -97
  93. data/Ent-Changes.md +0 -238
  94. data/Gemfile +0 -23
  95. data/Pro-2.0-Upgrade.md +0 -138
  96. data/Pro-3.0-Upgrade.md +0 -44
  97. data/Pro-4.0-Upgrade.md +0 -35
  98. data/Pro-Changes.md +0 -759
  99. data/Rakefile +0 -9
  100. data/bin/sidekiqctl +0 -20
  101. data/code_of_conduct.md +0 -50
  102. data/lib/generators/sidekiq/worker_generator.rb +0 -49
  103. data/lib/sidekiq/core_ext.rb +0 -1
  104. data/lib/sidekiq/ctl.rb +0 -221
  105. data/lib/sidekiq/logging.rb +0 -122
  106. data/lib/sidekiq/middleware/server/active_record.rb +0 -23
@@ -1,21 +1,28 @@
1
1
  # frozen_string_literal: true
2
- require 'sidekiq/manager'
3
- require 'sidekiq/fetch'
4
- require 'sidekiq/scheduled'
2
+
3
+ require "sidekiq/manager"
4
+ require "sidekiq/fetch"
5
+ require "sidekiq/scheduled"
5
6
 
6
7
  module Sidekiq
7
- # The Launcher is a very simple Actor whose job is to
8
- # start, monitor and stop the core Actors in Sidekiq.
9
- # If any of these actors die, the Sidekiq process exits
10
- # immediately.
8
+ # The Launcher starts the Manager and Poller threads and provides the process heartbeat.
11
9
  class Launcher
12
10
  include Util
13
11
 
14
- attr_accessor :manager, :poller, :fetcher
12
+ STATS_TTL = 5 * 365 * 24 * 60 * 60 # 5 years
15
13
 
16
- STATS_TTL = 5*365*24*60*60
14
+ PROCTITLES = [
15
+ proc { "sidekiq" },
16
+ proc { Sidekiq::VERSION },
17
+ proc { |me, data| data["tag"] },
18
+ proc { |me, data| "[#{Processor::WORKER_STATE.size} of #{data["concurrency"]} busy]" },
19
+ proc { |me, data| "stopping" if me.stopping? }
20
+ ]
21
+
22
+ attr_accessor :manager, :poller, :fetcher
17
23
 
18
24
  def initialize(options)
25
+ options[:fetch] ||= BasicFetch.new(options)
19
26
  @manager = Sidekiq::Manager.new(options)
20
27
  @poller = Sidekiq::Scheduled::Poller.new
21
28
  @done = false
@@ -50,7 +57,7 @@ module Sidekiq
50
57
 
51
58
  # Requeue everything in case there was a worker who grabbed work while stopped
52
59
  # This call is a no-op in Sidekiq but necessary for Sidekiq Pro.
53
- strategy = (@options[:fetch] || Sidekiq::BasicFetch)
60
+ strategy = @options[:fetch]
54
61
  strategy.bulk_requeue([], @options)
55
62
 
56
63
  clear_heartbeat
@@ -62,17 +69,66 @@ module Sidekiq
62
69
 
63
70
  private unless $TESTING
64
71
 
72
+ BEAT_PAUSE = 5
73
+
74
+ def start_heartbeat
75
+ loop do
76
+ heartbeat
77
+ sleep BEAT_PAUSE
78
+ end
79
+ Sidekiq.logger.info("Heartbeat stopping...")
80
+ end
81
+
82
+ def clear_heartbeat
83
+ # Remove record from Redis since we are shutting down.
84
+ # Note we don't stop the heartbeat thread; if the process
85
+ # doesn't actually exit, it'll reappear in the Web UI.
86
+ Sidekiq.redis do |conn|
87
+ conn.pipelined do
88
+ conn.srem("processes", identity)
89
+ conn.unlink("#{identity}:workers")
90
+ end
91
+ end
92
+ rescue
93
+ # best effort, ignore network errors
94
+ end
95
+
65
96
  def heartbeat
66
- results = Sidekiq::CLI::PROCTITLES.map {|x| x.(self, to_data) }
67
- results.compact!
68
- $0 = results.join(' ')
97
+ $0 = PROCTITLES.map { |proc| proc.call(self, to_data) }.compact.join(" ")
69
98
 
70
99
 
71
100
  end
72
101
 
102
+ def self.flush_stats
103
+ fails = Processor::FAILURE.reset
104
+ procd = Processor::PROCESSED.reset
105
+ return if fails + procd == 0
106
+
107
+ nowdate = Time.now.utc.strftime("%Y-%m-%d")
108
+ begin
109
+ Sidekiq.redis do |conn|
110
+ conn.pipelined do
111
+ conn.incrby("stat:processed", procd)
112
+ conn.incrby("stat:processed:#{nowdate}", procd)
113
+ conn.expire("stat:processed:#{nowdate}", STATS_TTL)
114
+
115
+ conn.incrby("stat:failed", fails)
116
+ conn.incrby("stat:failed:#{nowdate}", fails)
117
+ conn.expire("stat:failed:#{nowdate}", STATS_TTL)
118
+ end
119
+ end
120
+ rescue => ex
121
+ # we're exiting the process, things might be shut down so don't
122
+ # try to handle the exception
123
+ Sidekiq.logger.warn("Unable to flush stats: #{ex}")
124
+ end
125
+ end
126
+ at_exit(&method(:flush_stats))
127
+
73
128
  def ❤
74
129
  key = identity
75
130
  fails = procd = 0
131
+
76
132
  begin
77
133
  fails = Processor::FAILURE.reset
78
134
  procd = Processor::PROCESSED.reset
@@ -80,6 +136,7 @@ module Sidekiq
80
136
 
81
137
  workers_key = "#{key}:workers"
82
138
  nowdate = Time.now.utc.strftime("%Y-%m-%d")
139
+
83
140
  Sidekiq.redis do |conn|
84
141
  conn.multi do
85
142
  conn.incrby("stat:processed", procd)
@@ -90,84 +147,117 @@ module Sidekiq
90
147
  conn.incrby("stat:failed:#{nowdate}", fails)
91
148
  conn.expire("stat:failed:#{nowdate}", STATS_TTL)
92
149
 
93
- conn.del(workers_key)
150
+ conn.unlink(workers_key)
94
151
  curstate.each_pair do |tid, hash|
95
152
  conn.hset(workers_key, tid, Sidekiq.dump_json(hash))
96
153
  end
97
154
  conn.expire(workers_key, 60)
98
155
  end
99
156
  end
157
+
158
+ rtt = check_rtt
159
+
100
160
  fails = procd = 0
161
+ kb = memory_usage(::Process.pid)
101
162
 
102
- _, exists, _, _, msg = Sidekiq.redis do |conn|
103
- conn.multi do
104
- conn.sadd('processes', key)
105
- conn.exists(key)
106
- conn.hmset(key, 'info', to_json, 'busy', curstate.size, 'beat', Time.now.to_f, 'quiet', @done)
163
+ _, exists, _, _, msg = Sidekiq.redis { |conn|
164
+ conn.multi {
165
+ conn.sadd("processes", key)
166
+ conn.exists?(key)
167
+ conn.hmset(key, "info", to_json,
168
+ "busy", curstate.size,
169
+ "beat", Time.now.to_f,
170
+ "rtt_us", rtt,
171
+ "quiet", @done,
172
+ "rss", kb)
107
173
  conn.expire(key, 60)
108
174
  conn.rpop("#{key}-signals")
109
- end
110
- end
175
+ }
176
+ }
111
177
 
112
178
  # first heartbeat or recovering from an outage and need to reestablish our heartbeat
113
- fire_event(:heartbeat) if !exists
179
+ fire_event(:heartbeat) unless exists
114
180
 
115
181
  return unless msg
116
182
 
117
- ::Process.kill(msg, $$)
183
+ ::Process.kill(msg, ::Process.pid)
118
184
  rescue => e
119
185
  # ignore all redis/network issues
120
- logger.error("heartbeat: #{e.message}")
186
+ logger.error("heartbeat: #{e}")
121
187
  # don't lose the counts if there was a network issue
122
188
  Processor::PROCESSED.incr(procd)
123
189
  Processor::FAILURE.incr(fails)
124
190
  end
125
191
  end
126
192
 
127
- def start_heartbeat
128
- while true
129
- heartbeat
130
- sleep 5
193
+ # We run the heartbeat every five seconds.
194
+ # Capture five samples of RTT, log a warning if each sample
195
+ # is above our warning threshold.
196
+ RTT_READINGS = RingBuffer.new(5)
197
+ RTT_WARNING_LEVEL = 50_000
198
+
199
+ def check_rtt
200
+ a = b = 0
201
+ Sidekiq.redis do |x|
202
+ a = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC, :microsecond)
203
+ x.ping
204
+ b = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC, :microsecond)
131
205
  end
132
- Sidekiq.logger.info("Heartbeat stopping...")
206
+ rtt = b - a
207
+ RTT_READINGS << rtt
208
+ # Ideal RTT for Redis is < 1000µs
209
+ # Workable is < 10,000µs
210
+ # Log a warning if it's a disaster.
211
+ if RTT_READINGS.all? { |x| x > RTT_WARNING_LEVEL }
212
+ Sidekiq.logger.warn <<~EOM
213
+ Your Redis network connection is performing extremely poorly.
214
+ Last RTT readings were #{RTT_READINGS.buffer.inspect}, ideally these should be < 1000.
215
+ Ensure Redis is running in the same AZ or datacenter as Sidekiq.
216
+ If these values are close to 100,000, that means your Sidekiq process may be
217
+ CPU overloaded; see https://github.com/mperham/sidekiq/discussions/5039
218
+ EOM
219
+ RTT_READINGS.reset
220
+ end
221
+ rtt
133
222
  end
134
223
 
135
- def to_data
136
- @data ||= begin
137
- {
138
- 'hostname' => hostname,
139
- 'started_at' => Time.now.to_f,
140
- 'pid' => $$,
141
- 'tag' => @options[:tag] || '',
142
- 'concurrency' => @options[:concurrency],
143
- 'queues' => @options[:queues].uniq,
144
- 'labels' => @options[:labels],
145
- 'identity' => identity,
146
- }
147
- end
224
+ MEMORY_GRABBER = case RUBY_PLATFORM
225
+ when /linux/
226
+ ->(pid) {
227
+ IO.readlines("/proc/#{$$}/status").each do |line|
228
+ next unless line.start_with?("VmRSS:")
229
+ break line.split[1].to_i
230
+ end
231
+ }
232
+ when /darwin|bsd/
233
+ ->(pid) {
234
+ `ps -o pid,rss -p #{pid}`.lines.last.split.last.to_i
235
+ }
236
+ else
237
+ ->(pid) { 0 }
148
238
  end
149
239
 
150
- def to_json
151
- @json ||= begin
152
- # this data changes infrequently so dump it to a string
153
- # now so we don't need to dump it every heartbeat.
154
- Sidekiq.dump_json(to_data)
155
- end
240
+ def memory_usage(pid)
241
+ MEMORY_GRABBER.call(pid)
156
242
  end
157
243
 
158
- def clear_heartbeat
159
- # Remove record from Redis since we are shutting down.
160
- # Note we don't stop the heartbeat thread; if the process
161
- # doesn't actually exit, it'll reappear in the Web UI.
162
- Sidekiq.redis do |conn|
163
- conn.pipelined do
164
- conn.srem('processes', identity)
165
- conn.del("#{identity}:workers")
166
- end
167
- end
168
- rescue
169
- # best effort, ignore network errors
244
+ def to_data
245
+ @data ||= {
246
+ "hostname" => hostname,
247
+ "started_at" => Time.now.to_f,
248
+ "pid" => ::Process.pid,
249
+ "tag" => @options[:tag] || "",
250
+ "concurrency" => @options[:concurrency],
251
+ "queues" => @options[:queues].uniq,
252
+ "labels" => @options[:labels],
253
+ "identity" => identity
254
+ }
170
255
  end
171
256
 
257
+ def to_json
258
+ # this data changes infrequently so dump it to a string
259
+ # now so we don't need to dump it every heartbeat.
260
+ @json ||= Sidekiq.dump_json(to_data)
261
+ end
172
262
  end
173
263
  end
@@ -0,0 +1,166 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "logger"
4
+ require "time"
5
+
6
+ module Sidekiq
7
+ module Context
8
+ def self.with(hash)
9
+ orig_context = current.dup
10
+ current.merge!(hash)
11
+ yield
12
+ ensure
13
+ Thread.current[:sidekiq_context] = orig_context
14
+ end
15
+
16
+ def self.current
17
+ Thread.current[:sidekiq_context] ||= {}
18
+ end
19
+ end
20
+
21
+ module LoggingUtils
22
+ LEVELS = {
23
+ "debug" => 0,
24
+ "info" => 1,
25
+ "warn" => 2,
26
+ "error" => 3,
27
+ "fatal" => 4
28
+ }
29
+ LEVELS.default_proc = proc do |_, level|
30
+ Sidekiq.logger.warn("Invalid log level: #{level.inspect}")
31
+ nil
32
+ end
33
+
34
+ def debug?
35
+ level <= 0
36
+ end
37
+
38
+ def info?
39
+ level <= 1
40
+ end
41
+
42
+ def warn?
43
+ level <= 2
44
+ end
45
+
46
+ def error?
47
+ level <= 3
48
+ end
49
+
50
+ def fatal?
51
+ level <= 4
52
+ end
53
+
54
+ def local_level
55
+ Thread.current[:sidekiq_log_level]
56
+ end
57
+
58
+ def local_level=(level)
59
+ case level
60
+ when Integer
61
+ Thread.current[:sidekiq_log_level] = level
62
+ when Symbol, String
63
+ Thread.current[:sidekiq_log_level] = LEVELS[level.to_s]
64
+ when nil
65
+ Thread.current[:sidekiq_log_level] = nil
66
+ else
67
+ raise ArgumentError, "Invalid log level: #{level.inspect}"
68
+ end
69
+ end
70
+
71
+ def level
72
+ local_level || super
73
+ end
74
+
75
+ # Change the thread-local level for the duration of the given block.
76
+ def log_at(level)
77
+ old_local_level = local_level
78
+ self.local_level = level
79
+ yield
80
+ ensure
81
+ self.local_level = old_local_level
82
+ end
83
+
84
+ # Redefined to check severity against #level, and thus the thread-local level, rather than +@level+.
85
+ # FIXME: Remove when the minimum Ruby version supports overriding Logger#level.
86
+ def add(severity, message = nil, progname = nil, &block)
87
+ severity ||= ::Logger::UNKNOWN
88
+ progname ||= @progname
89
+
90
+ return true if @logdev.nil? || severity < level
91
+
92
+ if message.nil?
93
+ if block
94
+ message = yield
95
+ else
96
+ message = progname
97
+ progname = @progname
98
+ end
99
+ end
100
+
101
+ @logdev.write format_message(format_severity(severity), Time.now, progname, message)
102
+ end
103
+ end
104
+
105
+ class Logger < ::Logger
106
+ include LoggingUtils
107
+
108
+ def initialize(*args, **kwargs)
109
+ super
110
+ self.formatter = Sidekiq.log_formatter
111
+ end
112
+
113
+ module Formatters
114
+ class Base < ::Logger::Formatter
115
+ def tid
116
+ Thread.current["sidekiq_tid"] ||= (Thread.current.object_id ^ ::Process.pid).to_s(36)
117
+ end
118
+
119
+ def ctx
120
+ Sidekiq::Context.current
121
+ end
122
+
123
+ def format_context
124
+ if ctx.any?
125
+ " " + ctx.compact.map { |k, v|
126
+ case v
127
+ when Array
128
+ "#{k}=#{v.join(",")}"
129
+ else
130
+ "#{k}=#{v}"
131
+ end
132
+ }.join(" ")
133
+ end
134
+ end
135
+ end
136
+
137
+ class Pretty < Base
138
+ def call(severity, time, program_name, message)
139
+ "#{time.utc.iso8601(3)} pid=#{::Process.pid} tid=#{tid}#{format_context} #{severity}: #{message}\n"
140
+ end
141
+ end
142
+
143
+ class WithoutTimestamp < Pretty
144
+ def call(severity, time, program_name, message)
145
+ "pid=#{::Process.pid} tid=#{tid}#{format_context} #{severity}: #{message}\n"
146
+ end
147
+ end
148
+
149
+ class JSON < Base
150
+ def call(severity, time, program_name, message)
151
+ hash = {
152
+ ts: time.utc.iso8601(3),
153
+ pid: ::Process.pid,
154
+ tid: tid,
155
+ lvl: severity,
156
+ msg: message
157
+ }
158
+ c = ctx
159
+ hash["ctx"] = c unless c.empty?
160
+
161
+ Sidekiq.dump_json(hash) << "\n"
162
+ end
163
+ end
164
+ end
165
+ end
166
+ end
@@ -1,12 +1,11 @@
1
1
  # frozen_string_literal: true
2
- require 'sidekiq/util'
3
- require 'sidekiq/processor'
4
- require 'sidekiq/fetch'
5
- require 'thread'
6
- require 'set'
7
2
 
8
- module Sidekiq
3
+ require "sidekiq/util"
4
+ require "sidekiq/processor"
5
+ require "sidekiq/fetch"
6
+ require "set"
9
7
 
8
+ module Sidekiq
10
9
  ##
11
10
  # The Manager is the central coordination point in Sidekiq, controlling
12
11
  # the lifecycle of the Processors.
@@ -27,7 +26,7 @@ module Sidekiq
27
26
  attr_reader :workers
28
27
  attr_reader :options
29
28
 
30
- def initialize(options={})
29
+ def initialize(options = {})
31
30
  logger.debug { options.inspect }
32
31
  @options = options
33
32
  @count = options[:concurrency] || 10
@@ -36,7 +35,7 @@ module Sidekiq
36
35
  @done = false
37
36
  @workers = Set.new
38
37
  @count.times do
39
- @workers << Processor.new(self)
38
+ @workers << Processor.new(self, options)
40
39
  end
41
40
  @plock = Mutex.new
42
41
  end
@@ -56,9 +55,6 @@ module Sidekiq
56
55
  fire_event(:quiet, reverse: true)
57
56
  end
58
57
 
59
- # hack for quicker development / testing environment #2774
60
- PAUSE_TIME = STDOUT.tty? ? 0.1 : 0.5
61
-
62
58
  def stop(deadline)
63
59
  quiet
64
60
  fire_event(:shutdown, reverse: true)
@@ -70,12 +66,7 @@ module Sidekiq
70
66
  return if @workers.empty?
71
67
 
72
68
  logger.info { "Pausing to allow workers to finish..." }
73
- remaining = deadline - ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
74
- while remaining > PAUSE_TIME
75
- return if @workers.empty?
76
- sleep PAUSE_TIME
77
- remaining = deadline - ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
78
- end
69
+ wait_for(deadline) { @workers.empty? }
79
70
  return if @workers.empty?
80
71
 
81
72
  hard_shutdown
@@ -91,7 +82,7 @@ module Sidekiq
91
82
  @plock.synchronize do
92
83
  @workers.delete(processor)
93
84
  unless @done
94
- p = Processor.new(self)
85
+ p = Processor.new(self, options)
95
86
  @workers << p
96
87
  p.start
97
88
  end
@@ -113,7 +104,7 @@ module Sidekiq
113
104
  end
114
105
 
115
106
  if cleanup.size > 0
116
- jobs = cleanup.map {|p| p.job }.compact
107
+ jobs = cleanup.map { |p| p.job }.compact
117
108
 
118
109
  logger.warn { "Terminating #{cleanup.size} busy worker threads" }
119
110
  logger.warn { "Work still in progress #{jobs.inspect}" }
@@ -124,14 +115,19 @@ module Sidekiq
124
115
  # contract says that jobs are run AT LEAST once. Process termination
125
116
  # is delayed until we're certain the jobs are back in Redis because
126
117
  # it is worse to lose a job than to run it twice.
127
- strategy = (@options[:fetch] || Sidekiq::BasicFetch)
118
+ strategy = @options[:fetch]
128
119
  strategy.bulk_requeue(jobs, @options)
129
120
  end
130
121
 
131
122
  cleanup.each do |processor|
132
123
  processor.kill
133
124
  end
134
- end
135
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? }
131
+ end
136
132
  end
137
133
  end
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module Sidekiq
3
4
  # Middleware is code configured to run before/after
4
5
  # a message is processed. It is patterned after Rack
@@ -66,7 +67,6 @@ module Sidekiq
66
67
  module Middleware
67
68
  class Chain
68
69
  include Enumerable
69
- attr_reader :entries
70
70
 
71
71
  def initialize_copy(copy)
72
72
  copy.instance_variable_set(:@entries, entries.dup)
@@ -77,21 +77,25 @@ module Sidekiq
77
77
  end
78
78
 
79
79
  def initialize
80
- @entries = []
80
+ @entries = nil
81
81
  yield self if block_given?
82
82
  end
83
83
 
84
+ def entries
85
+ @entries ||= []
86
+ end
87
+
84
88
  def remove(klass)
85
89
  entries.delete_if { |entry| entry.klass == klass }
86
90
  end
87
91
 
88
92
  def add(klass, *args)
89
- remove(klass) if exists?(klass)
93
+ remove(klass)
90
94
  entries << Entry.new(klass, *args)
91
95
  end
92
96
 
93
97
  def prepend(klass, *args)
94
- remove(klass) if exists?(klass)
98
+ remove(klass)
95
99
  entries.insert(0, Entry.new(klass, *args))
96
100
  end
97
101
 
@@ -106,13 +110,17 @@ module Sidekiq
106
110
  i = entries.index { |entry| entry.klass == newklass }
107
111
  new_entry = i.nil? ? Entry.new(newklass, *args) : entries.delete_at(i)
108
112
  i = entries.index { |entry| entry.klass == oldklass } || entries.count - 1
109
- entries.insert(i+1, new_entry)
113
+ entries.insert(i + 1, new_entry)
110
114
  end
111
115
 
112
116
  def exists?(klass)
113
117
  any? { |entry| entry.klass == klass }
114
118
  end
115
119
 
120
+ def empty?
121
+ @entries.nil? || @entries.empty?
122
+ end
123
+
116
124
  def retrieve
117
125
  map(&:make_new)
118
126
  end
@@ -122,8 +130,10 @@ module Sidekiq
122
130
  end
123
131
 
124
132
  def invoke(*args)
125
- chain = retrieve.dup
126
- traverse_chain = lambda do
133
+ return yield if empty?
134
+
135
+ chain = retrieve
136
+ traverse_chain = proc do
127
137
  if chain.empty?
128
138
  yield
129
139
  else
@@ -134,12 +144,14 @@ module Sidekiq
134
144
  end
135
145
  end
136
146
 
147
+ private
148
+
137
149
  class Entry
138
150
  attr_reader :klass
139
151
 
140
152
  def initialize(klass, *args)
141
153
  @klass = klass
142
- @args = args
154
+ @args = args
143
155
  end
144
156
 
145
157
  def make_new
@@ -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