sidekiq 6.3.1 → 7.1.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (120) hide show
  1. checksums.yaml +4 -4
  2. data/Changes.md +285 -11
  3. data/LICENSE.txt +9 -0
  4. data/README.md +47 -34
  5. data/bin/sidekiq +4 -9
  6. data/bin/sidekiqload +207 -117
  7. data/bin/sidekiqmon +4 -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 +333 -190
  13. data/lib/sidekiq/capsule.rb +127 -0
  14. data/lib/sidekiq/cli.rb +86 -80
  15. data/lib/sidekiq/client.rb +104 -95
  16. data/lib/sidekiq/{util.rb → component.rb} +14 -41
  17. data/lib/sidekiq/config.rb +282 -0
  18. data/lib/sidekiq/deploy.rb +62 -0
  19. data/lib/sidekiq/embedded.rb +61 -0
  20. data/lib/sidekiq/fetch.rb +23 -24
  21. data/lib/sidekiq/job.rb +371 -10
  22. data/lib/sidekiq/job_logger.rb +16 -28
  23. data/lib/sidekiq/job_retry.rb +99 -58
  24. data/lib/sidekiq/job_util.rb +107 -0
  25. data/lib/sidekiq/launcher.rb +103 -95
  26. data/lib/sidekiq/logger.rb +9 -44
  27. data/lib/sidekiq/manager.rb +40 -41
  28. data/lib/sidekiq/metrics/query.rb +153 -0
  29. data/lib/sidekiq/metrics/shared.rb +95 -0
  30. data/lib/sidekiq/metrics/tracking.rb +136 -0
  31. data/lib/sidekiq/middleware/chain.rb +96 -51
  32. data/lib/sidekiq/middleware/current_attributes.rb +59 -16
  33. data/lib/sidekiq/middleware/i18n.rb +6 -4
  34. data/lib/sidekiq/middleware/modules.rb +21 -0
  35. data/lib/sidekiq/monitor.rb +17 -4
  36. data/lib/sidekiq/paginator.rb +17 -9
  37. data/lib/sidekiq/processor.rb +81 -80
  38. data/lib/sidekiq/rails.rb +21 -14
  39. data/lib/sidekiq/redis_client_adapter.rb +95 -0
  40. data/lib/sidekiq/redis_connection.rb +14 -82
  41. data/lib/sidekiq/ring_buffer.rb +29 -0
  42. data/lib/sidekiq/scheduled.rb +76 -38
  43. data/lib/sidekiq/testing/inline.rb +4 -4
  44. data/lib/sidekiq/testing.rb +42 -69
  45. data/lib/sidekiq/transaction_aware_client.rb +44 -0
  46. data/lib/sidekiq/version.rb +2 -1
  47. data/lib/sidekiq/web/action.rb +3 -3
  48. data/lib/sidekiq/web/application.rb +95 -11
  49. data/lib/sidekiq/web/csrf_protection.rb +4 -4
  50. data/lib/sidekiq/web/helpers.rb +58 -30
  51. data/lib/sidekiq/web.rb +22 -17
  52. data/lib/sidekiq/worker_compatibility_alias.rb +13 -0
  53. data/lib/sidekiq.rb +85 -202
  54. data/sidekiq.gemspec +12 -10
  55. data/web/assets/javascripts/application.js +77 -26
  56. data/web/assets/javascripts/base-charts.js +106 -0
  57. data/web/assets/javascripts/chart.min.js +13 -0
  58. data/web/assets/javascripts/chartjs-plugin-annotation.min.js +7 -0
  59. data/web/assets/javascripts/dashboard-charts.js +168 -0
  60. data/web/assets/javascripts/dashboard.js +3 -240
  61. data/web/assets/javascripts/metrics.js +264 -0
  62. data/web/assets/stylesheets/application-dark.css +17 -17
  63. data/web/assets/stylesheets/application-rtl.css +2 -91
  64. data/web/assets/stylesheets/application.css +69 -302
  65. data/web/locales/ar.yml +70 -70
  66. data/web/locales/cs.yml +62 -62
  67. data/web/locales/da.yml +60 -53
  68. data/web/locales/de.yml +65 -65
  69. data/web/locales/el.yml +43 -24
  70. data/web/locales/en.yml +84 -69
  71. data/web/locales/es.yml +68 -68
  72. data/web/locales/fa.yml +65 -65
  73. data/web/locales/fr.yml +81 -67
  74. data/web/locales/gd.yml +99 -0
  75. data/web/locales/he.yml +65 -64
  76. data/web/locales/hi.yml +59 -59
  77. data/web/locales/it.yml +53 -53
  78. data/web/locales/ja.yml +73 -68
  79. data/web/locales/ko.yml +52 -52
  80. data/web/locales/lt.yml +66 -66
  81. data/web/locales/nb.yml +61 -61
  82. data/web/locales/nl.yml +52 -52
  83. data/web/locales/pl.yml +45 -45
  84. data/web/locales/pt-br.yml +83 -55
  85. data/web/locales/pt.yml +51 -51
  86. data/web/locales/ru.yml +67 -66
  87. data/web/locales/sv.yml +53 -53
  88. data/web/locales/ta.yml +60 -60
  89. data/web/locales/uk.yml +62 -61
  90. data/web/locales/ur.yml +64 -64
  91. data/web/locales/vi.yml +67 -67
  92. data/web/locales/zh-cn.yml +43 -16
  93. data/web/locales/zh-tw.yml +42 -8
  94. data/web/views/_footer.erb +5 -2
  95. data/web/views/_job_info.erb +18 -2
  96. data/web/views/_metrics_period_select.erb +12 -0
  97. data/web/views/_nav.erb +1 -1
  98. data/web/views/_paging.erb +2 -0
  99. data/web/views/_poll_link.erb +1 -1
  100. data/web/views/_summary.erb +1 -1
  101. data/web/views/busy.erb +44 -28
  102. data/web/views/dashboard.erb +36 -4
  103. data/web/views/filtering.erb +7 -0
  104. data/web/views/metrics.erb +82 -0
  105. data/web/views/metrics_for_job.erb +68 -0
  106. data/web/views/morgue.erb +5 -9
  107. data/web/views/queue.erb +15 -15
  108. data/web/views/queues.erb +3 -1
  109. data/web/views/retries.erb +5 -9
  110. data/web/views/scheduled.erb +12 -13
  111. metadata +62 -31
  112. data/LICENSE +0 -9
  113. data/lib/generators/sidekiq/worker_generator.rb +0 -57
  114. data/lib/sidekiq/delay.rb +0 -41
  115. data/lib/sidekiq/exception_handler.rb +0 -27
  116. data/lib/sidekiq/extensions/action_mailer.rb +0 -48
  117. data/lib/sidekiq/extensions/active_record.rb +0 -43
  118. data/lib/sidekiq/extensions/class_methods.rb +0 -43
  119. data/lib/sidekiq/extensions/generic_proxy.rb +0 -33
  120. data/lib/sidekiq/worker.rb +0 -311
@@ -1,13 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "sidekiq/manager"
4
- require "sidekiq/fetch"
4
+ require "sidekiq/capsule"
5
5
  require "sidekiq/scheduled"
6
+ require "sidekiq/ring_buffer"
6
7
 
7
8
  module Sidekiq
8
- # The Launcher starts the Manager and Poller threads and provides the process heartbeat.
9
+ # The Launcher starts the Capsule Managers, the Poller thread 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,50 +16,56 @@ 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 #{me.config.total_concurrency} busy]" },
19
20
  proc { |me, data| "stopping" if me.stopping? }
20
21
  ]
21
22
 
22
- attr_accessor :manager, :poller, :fetcher
23
+ attr_accessor :managers, :poller
23
24
 
24
- def initialize(options)
25
- options[:fetch] ||= BasicFetch.new(options)
26
- @manager = Sidekiq::Manager.new(options)
27
- @poller = Sidekiq::Scheduled::Poller.new
25
+ def initialize(config, embedded: false)
26
+ @config = config
27
+ @embedded = embedded
28
+ @managers = config.capsules.values.map do |cap|
29
+ Sidekiq::Manager.new(cap)
30
+ end
31
+ @poller = Sidekiq::Scheduled::Poller.new(@config)
28
32
  @done = false
29
- @options = options
30
33
  end
31
34
 
32
- def run
33
- @thread = safe_thread("heartbeat", &method(:start_heartbeat))
35
+ # Start this Sidekiq instance. If an embedding process already
36
+ # has a heartbeat thread, caller can use `async_beat: false`
37
+ # and instead have thread call Launcher#heartbeat every N seconds.
38
+ def run(async_beat: true)
39
+ Sidekiq.freeze!
40
+ logger.debug { @config.merge!({}) }
41
+ @thread = safe_thread("heartbeat", &method(:start_heartbeat)) if async_beat
34
42
  @poller.start
35
- @manager.start
43
+ @managers.each(&:start)
36
44
  end
37
45
 
38
46
  # Stops this instance from processing any more jobs,
39
- #
40
47
  def quiet
48
+ return if @done
49
+
41
50
  @done = true
42
- @manager.quiet
51
+ @managers.each(&:quiet)
43
52
  @poller.terminate
53
+ fire_event(:quiet, reverse: true)
44
54
  end
45
55
 
46
- # Shuts down the process. This method does not
47
- # return until all work is complete and cleaned up.
48
- # It can take up to the timeout to complete.
56
+ # Shuts down this Sidekiq instance. Waits up to the deadline for all jobs to complete.
49
57
  def stop
50
- deadline = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC) + @options[:timeout]
58
+ deadline = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC) + @config[:timeout]
51
59
 
52
- @done = true
53
- @manager.quiet
54
- @poller.terminate
55
-
56
- @manager.stop(deadline)
60
+ quiet
61
+ stoppers = @managers.map do |mgr|
62
+ Thread.new do
63
+ mgr.stop(deadline)
64
+ end
65
+ end
57
66
 
58
- # Requeue everything in case there was a worker who grabbed work while stopped
59
- # This call is a no-op in Sidekiq but necessary for Sidekiq Pro.
60
- strategy = @options[:fetch]
61
- strategy.bulk_requeue([], @options)
67
+ fire_event(:shutdown, reverse: true)
68
+ stoppers.each(&:join)
62
69
 
63
70
  clear_heartbeat
64
71
  end
@@ -67,91 +74,86 @@ module Sidekiq
67
74
  @done
68
75
  end
69
76
 
77
+ # If embedding Sidekiq, you can have the process heartbeat
78
+ # call this method to regularly heartbeat rather than creating
79
+ # a separate thread.
80
+ def heartbeat
81
+
82
+ end
83
+
70
84
  private unless $TESTING
71
85
 
72
- BEAT_PAUSE = 5
86
+ BEAT_PAUSE = 10
73
87
 
74
88
  def start_heartbeat
75
89
  loop do
76
- heartbeat
90
+ beat
77
91
  sleep BEAT_PAUSE
78
92
  end
79
- Sidekiq.logger.info("Heartbeat stopping...")
93
+ logger.info("Heartbeat stopping...")
94
+ end
95
+
96
+ def beat
97
+ $0 = PROCTITLES.map { |proc| proc.call(self, to_data) }.compact.join(" ") unless @embedded
98
+
80
99
  end
81
100
 
82
101
  def clear_heartbeat
102
+ flush_stats
103
+
83
104
  # Remove record from Redis since we are shutting down.
84
105
  # Note we don't stop the heartbeat thread; if the process
85
106
  # 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")
107
+ redis do |conn|
108
+ conn.pipelined do |pipeline|
109
+ pipeline.srem("processes", [identity])
110
+ pipeline.unlink("#{identity}:work")
90
111
  end
91
112
  end
92
113
  rescue
93
114
  # best effort, ignore network errors
94
115
  end
95
116
 
96
- def heartbeat
97
- $0 = PROCTITLES.map { |proc| proc.call(self, to_data) }.compact.join(" ")
98
-
99
-
100
- end
101
-
102
- def self.flush_stats
117
+ def flush_stats
103
118
  fails = Processor::FAILURE.reset
104
119
  procd = Processor::PROCESSED.reset
105
120
  return if fails + procd == 0
106
121
 
107
122
  nowdate = Time.now.utc.strftime("%Y-%m-%d")
108
123
  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)
124
+ redis do |conn|
125
+ conn.pipelined do |pipeline|
126
+ pipeline.incrby("stat:processed", procd)
127
+ pipeline.incrby("stat:processed:#{nowdate}", procd)
128
+ pipeline.expire("stat:processed:#{nowdate}", STATS_TTL)
129
+
130
+ pipeline.incrby("stat:failed", fails)
131
+ pipeline.incrby("stat:failed:#{nowdate}", fails)
132
+ pipeline.expire("stat:failed:#{nowdate}", STATS_TTL)
118
133
  end
119
134
  end
120
135
  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}")
136
+ logger.warn("Unable to flush stats: #{ex}")
124
137
  end
125
138
  end
126
- at_exit(&method(:flush_stats))
127
139
 
128
140
  def ❤
129
141
  key = identity
130
142
  fails = procd = 0
131
143
 
132
144
  begin
133
- fails = Processor::FAILURE.reset
134
- procd = Processor::PROCESSED.reset
135
- curstate = Processor::WORKER_STATE.dup
136
-
137
- workers_key = "#{key}:workers"
138
- nowdate = Time.now.utc.strftime("%Y-%m-%d")
139
-
140
- Sidekiq.redis do |conn|
141
- conn.multi do
142
- conn.incrby("stat:processed", procd)
143
- conn.incrby("stat:processed:#{nowdate}", procd)
144
- conn.expire("stat:processed:#{nowdate}", STATS_TTL)
145
-
146
- conn.incrby("stat:failed", fails)
147
- conn.incrby("stat:failed:#{nowdate}", fails)
148
- conn.expire("stat:failed:#{nowdate}", STATS_TTL)
149
-
150
- conn.unlink(workers_key)
145
+ flush_stats
146
+
147
+ curstate = Processor::WORK_STATE.dup
148
+ redis do |conn|
149
+ # work is the current set of executing jobs
150
+ work_key = "#{key}:work"
151
+ conn.pipelined do |transaction|
152
+ transaction.unlink(work_key)
151
153
  curstate.each_pair do |tid, hash|
152
- conn.hset(workers_key, tid, Sidekiq.dump_json(hash))
154
+ transaction.hset(work_key, tid, Sidekiq.dump_json(hash))
153
155
  end
154
- conn.expire(workers_key, 60)
156
+ transaction.expire(work_key, 60)
155
157
  end
156
158
  end
157
159
 
@@ -160,27 +162,26 @@ module Sidekiq
160
162
  fails = procd = 0
161
163
  kb = memory_usage(::Process.pid)
162
164
 
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,
165
+ _, exists, _, _, signal = redis { |conn|
166
+ conn.multi { |transaction|
167
+ transaction.sadd("processes", [key])
168
+ transaction.exists(key)
169
+ transaction.hset(key, "info", to_json,
168
170
  "busy", curstate.size,
169
171
  "beat", Time.now.to_f,
170
172
  "rtt_us", rtt,
171
- "quiet", @done,
173
+ "quiet", @done.to_s,
172
174
  "rss", kb)
173
- conn.expire(key, 60)
174
- conn.rpop("#{key}-signals")
175
+ transaction.expire(key, 60)
176
+ transaction.rpop("#{key}-signals")
175
177
  }
176
178
  }
177
179
 
178
180
  # first heartbeat or recovering from an outage and need to reestablish our heartbeat
179
- fire_event(:heartbeat) unless exists
181
+ fire_event(:heartbeat) unless exists > 0
182
+ fire_event(:beat, oneshot: false)
180
183
 
181
- return unless msg
182
-
183
- ::Process.kill(msg, ::Process.pid)
184
+ ::Process.kill(signal, ::Process.pid) if signal && !@embedded
184
185
  rescue => e
185
186
  # ignore all redis/network issues
186
187
  logger.error("heartbeat: #{e}")
@@ -198,7 +199,7 @@ module Sidekiq
198
199
 
199
200
  def check_rtt
200
201
  a = b = 0
201
- Sidekiq.redis do |x|
202
+ redis do |x|
202
203
  a = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC, :microsecond)
203
204
  x.ping
204
205
  b = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC, :microsecond)
@@ -209,12 +210,12 @@ module Sidekiq
209
210
  # Workable is < 10,000µs
210
211
  # Log a warning if it's a disaster.
211
212
  if RTT_READINGS.all? { |x| x > RTT_WARNING_LEVEL }
212
- Sidekiq.logger.warn <<~EOM
213
+ logger.warn <<~EOM
213
214
  Your Redis network connection is performing extremely poorly.
214
215
  Last RTT readings were #{RTT_READINGS.buffer.inspect}, ideally these should be < 1000.
215
216
  Ensure Redis is running in the same AZ or datacenter as Sidekiq.
216
217
  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
+ CPU-saturated; reduce your concurrency and/or see https://github.com/sidekiq/sidekiq/discussions/5039
218
219
  EOM
219
220
  RTT_READINGS.reset
220
221
  end
@@ -246,14 +247,21 @@ module Sidekiq
246
247
  "hostname" => hostname,
247
248
  "started_at" => Time.now.to_f,
248
249
  "pid" => ::Process.pid,
249
- "tag" => @options[:tag] || "",
250
- "concurrency" => @options[:concurrency],
251
- "queues" => @options[:queues].uniq,
252
- "labels" => @options[:labels],
253
- "identity" => identity
250
+ "tag" => @config[:tag] || "",
251
+ "concurrency" => @config.total_concurrency,
252
+ "queues" => @config.capsules.values.flat_map { |cap| cap.queues }.uniq,
253
+ "weights" => to_weights,
254
+ "labels" => @config[:labels].to_a,
255
+ "identity" => identity,
256
+ "version" => Sidekiq::VERSION,
257
+ "embedded" => @embedded
254
258
  }
255
259
  end
256
260
 
261
+ def to_weights
262
+ @config.capsules.values.map(&:weights)
263
+ end
264
+
257
265
  def to_json
258
266
  # this data changes infrequently so dump it to a string
259
267
  # now so we don't need to dump it every heartbeat.
@@ -16,6 +16,10 @@ module Sidekiq
16
16
  def self.current
17
17
  Thread.current[:sidekiq_context] ||= {}
18
18
  end
19
+
20
+ def self.add(k, v)
21
+ current[k] = v
22
+ end
19
23
  end
20
24
 
21
25
  module LoggingUtils
@@ -27,28 +31,14 @@ module Sidekiq
27
31
  "fatal" => 4
28
32
  }
29
33
  LEVELS.default_proc = proc do |_, level|
30
- Sidekiq.logger.warn("Invalid log level: #{level.inspect}")
34
+ puts("Invalid log level: #{level.inspect}")
31
35
  nil
32
36
  end
33
37
 
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
38
+ LEVELS.each do |level, numeric_level|
39
+ define_method("#{level}?") do
40
+ local_level.nil? ? super() : local_level <= numeric_level
41
+ end
52
42
  end
53
43
 
54
44
  def local_level
@@ -80,36 +70,11 @@ module Sidekiq
80
70
  ensure
81
71
  self.local_level = old_local_level
82
72
  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
73
  end
104
74
 
105
75
  class Logger < ::Logger
106
76
  include LoggingUtils
107
77
 
108
- def initialize(*args, **kwargs)
109
- super
110
- self.formatter = Sidekiq.log_formatter
111
- end
112
-
113
78
  module Formatters
114
79
  class Base < ::Logger::Formatter
115
80
  def tid
@@ -1,8 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "sidekiq/util"
4
3
  require "sidekiq/processor"
5
- require "sidekiq/fetch"
6
4
  require "set"
7
5
 
8
6
  module Sidekiq
@@ -21,46 +19,38 @@ module Sidekiq
21
19
  # the shutdown process. The other tasks are performed by other threads.
22
20
  #
23
21
  class Manager
24
- include Util
22
+ include Sidekiq::Component
25
23
 
26
24
  attr_reader :workers
27
- attr_reader :options
25
+ attr_reader :capsule
28
26
 
29
- def initialize(options = {})
30
- logger.debug { options.inspect }
31
- @options = options
32
- @count = options[:concurrency] || 10
27
+ def initialize(capsule)
28
+ @config = @capsule = capsule
29
+ @count = capsule.concurrency
33
30
  raise ArgumentError, "Concurrency of #{@count} is not supported" if @count < 1
34
31
 
35
32
  @done = false
36
33
  @workers = Set.new
34
+ @plock = Mutex.new
37
35
  @count.times do
38
- @workers << Processor.new(self, options)
36
+ @workers << Processor.new(@config, &method(:processor_result))
39
37
  end
40
- @plock = Mutex.new
41
38
  end
42
39
 
43
40
  def start
44
- @workers.each do |x|
45
- x.start
46
- end
41
+ @workers.each(&:start)
47
42
  end
48
43
 
49
44
  def quiet
50
45
  return if @done
51
46
  @done = true
52
47
 
53
- logger.info { "Terminating quiet workers" }
54
- @workers.each { |x| x.terminate }
55
- fire_event(:quiet, reverse: true)
48
+ logger.info { "Terminating quiet threads for #{capsule.name} capsule" }
49
+ @workers.each(&:terminate)
56
50
  end
57
51
 
58
- # hack for quicker development / testing environment #2774
59
- PAUSE_TIME = $stdout.tty? ? 0.1 : 0.5
60
-
61
52
  def stop(deadline)
62
53
  quiet
63
- fire_event(:shutdown, reverse: true)
64
54
 
65
55
  # some of the shutdown events can be async,
66
56
  # we don't have any way to know when they're done but
@@ -68,29 +58,20 @@ module Sidekiq
68
58
  sleep PAUSE_TIME
69
59
  return if @workers.empty?
70
60
 
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
61
+ logger.info { "Pausing to allow jobs to finish..." }
62
+ wait_for(deadline) { @workers.empty? }
78
63
  return if @workers.empty?
79
64
 
80
65
  hard_shutdown
66
+ ensure
67
+ capsule.stop
81
68
  end
82
69
 
83
- def processor_stopped(processor)
84
- @plock.synchronize do
85
- @workers.delete(processor)
86
- end
87
- end
88
-
89
- def processor_died(processor, reason)
70
+ def processor_result(processor, reason = nil)
90
71
  @plock.synchronize do
91
72
  @workers.delete(processor)
92
73
  unless @done
93
- p = Processor.new(self, options)
74
+ p = Processor.new(@config, &method(:processor_result))
94
75
  @workers << p
95
76
  p.start
96
77
  end
@@ -104,7 +85,7 @@ module Sidekiq
104
85
  private
105
86
 
106
87
  def hard_shutdown
107
- # We've reached the timeout and we still have busy workers.
88
+ # We've reached the timeout and we still have busy threads.
108
89
  # They must die but their jobs shall live on.
109
90
  cleanup = nil
110
91
  @plock.synchronize do
@@ -114,22 +95,40 @@ module Sidekiq
114
95
  if cleanup.size > 0
115
96
  jobs = cleanup.map { |p| p.job }.compact
116
97
 
117
- logger.warn { "Terminating #{cleanup.size} busy worker threads" }
118
- logger.warn { "Work still in progress #{jobs.inspect}" }
98
+ logger.warn { "Terminating #{cleanup.size} busy threads" }
99
+ logger.debug { "Jobs still in progress #{jobs.inspect}" }
119
100
 
120
101
  # Re-enqueue unfinished jobs
121
102
  # 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
103
+ # the thread is terminated. This is ok because Sidekiq's
123
104
  # contract says that jobs are run AT LEAST once. Process termination
124
105
  # is delayed until we're certain the jobs are back in Redis because
125
106
  # it is worse to lose a job than to run it twice.
126
- strategy = @options[:fetch]
127
- strategy.bulk_requeue(jobs, @options)
107
+ capsule.fetcher.bulk_requeue(jobs)
128
108
  end
129
109
 
130
110
  cleanup.each do |processor|
131
111
  processor.kill
132
112
  end
113
+
114
+ # when this method returns, we immediately call `exit` which may not give
115
+ # the remaining threads time to run `ensure` blocks, etc. We pause here up
116
+ # to 3 seconds to give threads a minimal amount of time to run `ensure` blocks.
117
+ deadline = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC) + 3
118
+ wait_for(deadline) { @workers.empty? }
119
+ end
120
+
121
+ # hack for quicker development / testing environment #2774
122
+ PAUSE_TIME = $stdout.tty? ? 0.1 : 0.5
123
+
124
+ # Wait for the orblock to be true or the deadline passed.
125
+ def wait_for(deadline, &condblock)
126
+ remaining = deadline - ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
127
+ while remaining > PAUSE_TIME
128
+ return if condblock.call
129
+ sleep PAUSE_TIME
130
+ remaining = deadline - ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
131
+ end
133
132
  end
134
133
  end
135
134
  end