sidekiq 5.1.3 → 6.4.1

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 +5 -5
  2. data/Changes.md +391 -1
  3. data/LICENSE +3 -3
  4. data/README.md +23 -34
  5. data/bin/sidekiq +27 -3
  6. data/bin/sidekiqload +68 -62
  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 +344 -243
  13. data/lib/sidekiq/cli.rb +209 -221
  14. data/lib/sidekiq/client.rb +62 -64
  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 +13 -22
  18. data/lib/sidekiq/extensions/active_record.rb +13 -10
  19. data/lib/sidekiq/extensions/class_methods.rb +14 -11
  20. data/lib/sidekiq/extensions/generic_proxy.rb +6 -4
  21. data/lib/sidekiq/fetch.rb +40 -32
  22. data/lib/sidekiq/job.rb +13 -0
  23. data/lib/sidekiq/job_logger.rb +35 -9
  24. data/lib/sidekiq/job_retry.rb +88 -68
  25. data/lib/sidekiq/job_util.rb +65 -0
  26. data/lib/sidekiq/launcher.rb +170 -73
  27. data/lib/sidekiq/logger.rb +170 -0
  28. data/lib/sidekiq/manager.rb +18 -22
  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 +20 -16
  34. data/lib/sidekiq/processor.rb +135 -83
  35. data/lib/sidekiq/rails.rb +42 -38
  36. data/lib/sidekiq/redis_connection.rb +49 -30
  37. data/lib/sidekiq/scheduled.rb +94 -31
  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 +40 -31
  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 +109 -74
  46. data/lib/sidekiq/web/csrf_protection.rb +180 -0
  47. data/lib/sidekiq/web/helpers.rb +117 -93
  48. data/lib/sidekiq/web/router.rb +23 -19
  49. data/lib/sidekiq/web.rb +61 -105
  50. data/lib/sidekiq/worker.rb +257 -99
  51. data/lib/sidekiq.rb +81 -46
  52. data/sidekiq.gemspec +23 -23
  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 +66 -75
  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 +77 -231
  59. data/web/assets/stylesheets/bootstrap.css +2 -2
  60. data/web/locales/ar.yml +9 -2
  61. data/web/locales/de.yml +14 -2
  62. data/web/locales/en.yml +7 -1
  63. data/web/locales/es.yml +21 -5
  64. data/web/locales/fr.yml +10 -3
  65. data/web/locales/ja.yml +7 -1
  66. data/web/locales/lt.yml +83 -0
  67. data/web/locales/pl.yml +4 -4
  68. data/web/locales/ru.yml +4 -0
  69. data/web/locales/vi.yml +83 -0
  70. data/web/views/_footer.erb +1 -1
  71. data/web/views/_job_info.erb +3 -2
  72. data/web/views/_nav.erb +3 -17
  73. data/web/views/_poll_link.erb +2 -5
  74. data/web/views/_summary.erb +7 -7
  75. data/web/views/busy.erb +54 -20
  76. data/web/views/dashboard.erb +22 -14
  77. data/web/views/dead.erb +3 -3
  78. data/web/views/layout.erb +4 -2
  79. data/web/views/morgue.erb +9 -6
  80. data/web/views/queue.erb +20 -10
  81. data/web/views/queues.erb +11 -3
  82. data/web/views/retries.erb +14 -7
  83. data/web/views/retry.erb +3 -3
  84. data/web/views/scheduled.erb +5 -2
  85. metadata +39 -136
  86. data/.github/contributing.md +0 -32
  87. data/.github/issue_template.md +0 -11
  88. data/.gitignore +0 -13
  89. data/.travis.yml +0 -14
  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/COMM-LICENSE +0 -95
  94. data/Ent-Changes.md +0 -216
  95. data/Gemfile +0 -8
  96. data/Pro-2.0-Upgrade.md +0 -138
  97. data/Pro-3.0-Upgrade.md +0 -44
  98. data/Pro-4.0-Upgrade.md +0 -35
  99. data/Pro-Changes.md +0 -729
  100. data/Rakefile +0 -8
  101. data/bin/sidekiqctl +0 -99
  102. data/code_of_conduct.md +0 -50
  103. data/lib/generators/sidekiq/worker_generator.rb +0 -49
  104. data/lib/sidekiq/core_ext.rb +0 -1
  105. data/lib/sidekiq/logging.rb +0 -122
  106. data/lib/sidekiq/middleware/server/active_record.rb +0 -23
@@ -1,19 +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
 
12
+ STATS_TTL = 5 * 365 * 24 * 60 * 60 # 5 years
13
+
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
+
14
22
  attr_accessor :manager, :poller, :fetcher
15
23
 
16
24
  def initialize(options)
25
+ options[:fetch] ||= BasicFetch.new(options)
17
26
  @manager = Sidekiq::Manager.new(options)
18
27
  @poller = Sidekiq::Scheduled::Poller.new
19
28
  @done = false
@@ -38,7 +47,7 @@ module Sidekiq
38
47
  # return until all work is complete and cleaned up.
39
48
  # It can take up to the timeout to complete.
40
49
  def stop
41
- deadline = Time.now + @options[:timeout]
50
+ deadline = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC) + @options[:timeout]
42
51
 
43
52
  @done = true
44
53
  @manager.quiet
@@ -48,7 +57,7 @@ module Sidekiq
48
57
 
49
58
  # Requeue everything in case there was a worker who grabbed work while stopped
50
59
  # This call is a no-op in Sidekiq but necessary for Sidekiq Pro.
51
- strategy = (@options[:fetch] || Sidekiq::BasicFetch)
60
+ strategy = @options[:fetch]
52
61
  strategy.bulk_requeue([], @options)
53
62
 
54
63
  clear_heartbeat
@@ -60,107 +69,195 @@ module Sidekiq
60
69
 
61
70
  private unless $TESTING
62
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 |pipeline|
88
+ pipeline.srem("processes", identity)
89
+ pipeline.unlink("#{identity}:workers")
90
+ end
91
+ end
92
+ rescue
93
+ # best effort, ignore network errors
94
+ end
95
+
63
96
  def heartbeat
64
- results = Sidekiq::CLI::PROCTITLES.map {|x| x.(self, to_data) }
65
- results.compact!
66
- $0 = results.join(' ')
97
+ $0 = PROCTITLES.map { |proc| proc.call(self, to_data) }.compact.join(" ")
67
98
 
68
99
 
69
100
  end
70
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 |pipeline|
111
+ pipeline.incrby("stat:processed", procd)
112
+ pipeline.incrby("stat:processed:#{nowdate}", procd)
113
+ pipeline.expire("stat:processed:#{nowdate}", STATS_TTL)
114
+
115
+ pipeline.incrby("stat:failed", fails)
116
+ pipeline.incrby("stat:failed:#{nowdate}", fails)
117
+ pipeline.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
+
71
128
  def ❤
72
129
  key = identity
73
130
  fails = procd = 0
131
+
74
132
  begin
75
- Processor::FAILURE.update {|curr| fails = curr; 0 }
76
- Processor::PROCESSED.update {|curr| procd = curr; 0 }
133
+ fails = Processor::FAILURE.reset
134
+ procd = Processor::PROCESSED.reset
135
+ curstate = Processor::WORKER_STATE.dup
77
136
 
78
137
  workers_key = "#{key}:workers"
79
138
  nowdate = Time.now.utc.strftime("%Y-%m-%d")
139
+
80
140
  Sidekiq.redis do |conn|
81
- conn.multi do
82
- conn.incrby("stat:processed", procd)
83
- conn.incrby("stat:processed:#{nowdate}", procd)
84
- conn.incrby("stat:failed", fails)
85
- conn.incrby("stat:failed:#{nowdate}", fails)
86
- conn.del(workers_key)
87
- Processor::WORKER_STATE.each_pair do |tid, hash|
88
- conn.hset(workers_key, tid, Sidekiq.dump_json(hash))
141
+ conn.multi do |transaction|
142
+ transaction.incrby("stat:processed", procd)
143
+ transaction.incrby("stat:processed:#{nowdate}", procd)
144
+ transaction.expire("stat:processed:#{nowdate}", STATS_TTL)
145
+
146
+ transaction.incrby("stat:failed", fails)
147
+ transaction.incrby("stat:failed:#{nowdate}", fails)
148
+ transaction.expire("stat:failed:#{nowdate}", STATS_TTL)
149
+
150
+ transaction.unlink(workers_key)
151
+ curstate.each_pair do |tid, hash|
152
+ transaction.hset(workers_key, tid, Sidekiq.dump_json(hash))
89
153
  end
90
- conn.expire(workers_key, 60)
154
+ transaction.expire(workers_key, 60)
91
155
  end
92
156
  end
157
+
158
+ rtt = check_rtt
159
+
93
160
  fails = procd = 0
161
+ kb = memory_usage(::Process.pid)
94
162
 
95
- _, exists, _, _, msg = Sidekiq.redis do |conn|
96
- conn.multi do
97
- conn.sadd('processes', key)
98
- conn.exists(key)
99
- conn.hmset(key, 'info', to_json, 'busy', Processor::WORKER_STATE.size, 'beat', Time.now.to_f, 'quiet', @done)
100
- conn.expire(key, 60)
101
- conn.rpop("#{key}-signals")
102
- end
103
- end
163
+ _, exists, _, _, msg = Sidekiq.redis { |conn|
164
+ conn.multi { |transaction|
165
+ transaction.sadd("processes", key)
166
+ transaction.exists?(key)
167
+ transaction.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)
173
+ transaction.expire(key, 60)
174
+ transaction.rpop("#{key}-signals")
175
+ }
176
+ }
104
177
 
105
178
  # first heartbeat or recovering from an outage and need to reestablish our heartbeat
106
- fire_event(:heartbeat) if !exists
179
+ fire_event(:heartbeat) unless exists
107
180
 
108
181
  return unless msg
109
182
 
110
- ::Process.kill(msg, $$)
183
+ ::Process.kill(msg, ::Process.pid)
111
184
  rescue => e
112
185
  # ignore all redis/network issues
113
- logger.error("heartbeat: #{e.message}")
186
+ logger.error("heartbeat: #{e}")
114
187
  # don't lose the counts if there was a network issue
115
- Processor::PROCESSED.increment(procd)
116
- Processor::FAILURE.increment(fails)
188
+ Processor::PROCESSED.incr(procd)
189
+ Processor::FAILURE.incr(fails)
117
190
  end
118
191
  end
119
192
 
120
- def start_heartbeat
121
- while true
122
- heartbeat
123
- 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)
124
205
  end
125
- 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
126
222
  end
127
223
 
128
- def to_data
129
- @data ||= begin
130
- {
131
- 'hostname' => hostname,
132
- 'started_at' => Time.now.to_f,
133
- 'pid' => $$,
134
- 'tag' => @options[:tag] || '',
135
- 'concurrency' => @options[:concurrency],
136
- 'queues' => @options[:queues].uniq,
137
- 'labels' => @options[:labels],
138
- 'identity' => identity,
139
- }
140
- 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 }
141
238
  end
142
239
 
143
- def to_json
144
- @json ||= begin
145
- # this data changes infrequently so dump it to a string
146
- # now so we don't need to dump it every heartbeat.
147
- Sidekiq.dump_json(to_data)
148
- end
240
+ def memory_usage(pid)
241
+ MEMORY_GRABBER.call(pid)
149
242
  end
150
243
 
151
- def clear_heartbeat
152
- # Remove record from Redis since we are shutting down.
153
- # Note we don't stop the heartbeat thread; if the process
154
- # doesn't actually exit, it'll reappear in the Web UI.
155
- Sidekiq.redis do |conn|
156
- conn.pipelined do
157
- conn.srem('processes', identity)
158
- conn.del("#{identity}:workers")
159
- end
160
- end
161
- rescue
162
- # 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
+ }
163
255
  end
164
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
165
262
  end
166
263
  end
@@ -0,0 +1,170 @@
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
+
20
+ def self.add(k, v)
21
+ Thread.current[:sidekiq_context][k] = v
22
+ end
23
+ end
24
+
25
+ module LoggingUtils
26
+ LEVELS = {
27
+ "debug" => 0,
28
+ "info" => 1,
29
+ "warn" => 2,
30
+ "error" => 3,
31
+ "fatal" => 4
32
+ }
33
+ LEVELS.default_proc = proc do |_, level|
34
+ Sidekiq.logger.warn("Invalid log level: #{level.inspect}")
35
+ nil
36
+ end
37
+
38
+ def debug?
39
+ level <= 0
40
+ end
41
+
42
+ def info?
43
+ level <= 1
44
+ end
45
+
46
+ def warn?
47
+ level <= 2
48
+ end
49
+
50
+ def error?
51
+ level <= 3
52
+ end
53
+
54
+ def fatal?
55
+ level <= 4
56
+ end
57
+
58
+ def local_level
59
+ Thread.current[:sidekiq_log_level]
60
+ end
61
+
62
+ def local_level=(level)
63
+ case level
64
+ when Integer
65
+ Thread.current[:sidekiq_log_level] = level
66
+ when Symbol, String
67
+ Thread.current[:sidekiq_log_level] = LEVELS[level.to_s]
68
+ when nil
69
+ Thread.current[:sidekiq_log_level] = nil
70
+ else
71
+ raise ArgumentError, "Invalid log level: #{level.inspect}"
72
+ end
73
+ end
74
+
75
+ def level
76
+ local_level || super
77
+ end
78
+
79
+ # Change the thread-local level for the duration of the given block.
80
+ def log_at(level)
81
+ old_local_level = local_level
82
+ self.local_level = level
83
+ yield
84
+ ensure
85
+ self.local_level = old_local_level
86
+ end
87
+
88
+ # Redefined to check severity against #level, and thus the thread-local level, rather than +@level+.
89
+ # FIXME: Remove when the minimum Ruby version supports overriding Logger#level.
90
+ def add(severity, message = nil, progname = nil, &block)
91
+ severity ||= ::Logger::UNKNOWN
92
+ progname ||= @progname
93
+
94
+ return true if @logdev.nil? || severity < level
95
+
96
+ if message.nil?
97
+ if block
98
+ message = yield
99
+ else
100
+ message = progname
101
+ progname = @progname
102
+ end
103
+ end
104
+
105
+ @logdev.write format_message(format_severity(severity), Time.now, progname, message)
106
+ end
107
+ end
108
+
109
+ class Logger < ::Logger
110
+ include LoggingUtils
111
+
112
+ def initialize(*args, **kwargs)
113
+ super
114
+ self.formatter = Sidekiq.log_formatter
115
+ end
116
+
117
+ module Formatters
118
+ class Base < ::Logger::Formatter
119
+ def tid
120
+ Thread.current["sidekiq_tid"] ||= (Thread.current.object_id ^ ::Process.pid).to_s(36)
121
+ end
122
+
123
+ def ctx
124
+ Sidekiq::Context.current
125
+ end
126
+
127
+ def format_context
128
+ if ctx.any?
129
+ " " + ctx.compact.map { |k, v|
130
+ case v
131
+ when Array
132
+ "#{k}=#{v.join(",")}"
133
+ else
134
+ "#{k}=#{v}"
135
+ end
136
+ }.join(" ")
137
+ end
138
+ end
139
+ end
140
+
141
+ class Pretty < Base
142
+ def call(severity, time, program_name, message)
143
+ "#{time.utc.iso8601(3)} pid=#{::Process.pid} tid=#{tid}#{format_context} #{severity}: #{message}\n"
144
+ end
145
+ end
146
+
147
+ class WithoutTimestamp < Pretty
148
+ def call(severity, time, program_name, message)
149
+ "pid=#{::Process.pid} tid=#{tid}#{format_context} #{severity}: #{message}\n"
150
+ end
151
+ end
152
+
153
+ class JSON < Base
154
+ def call(severity, time, program_name, message)
155
+ hash = {
156
+ ts: time.utc.iso8601(3),
157
+ pid: ::Process.pid,
158
+ tid: tid,
159
+ lvl: severity,
160
+ msg: message
161
+ }
162
+ c = ctx
163
+ hash["ctx"] = c unless c.empty?
164
+
165
+ Sidekiq.dump_json(hash) << "\n"
166
+ end
167
+ end
168
+ end
169
+ end
170
+ 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,16 +26,16 @@ 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
- @count = options[:concurrency] || 25
32
+ @count = options[:concurrency] || 10
34
33
  raise ArgumentError, "Concurrency of #{@count} is not supported" if @count < 1
35
34
 
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 - Time.now
74
- while remaining > PAUSE_TIME
75
- return if @workers.empty?
76
- sleep PAUSE_TIME
77
- remaining = deadline - Time.now
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