sidekiq 6.1.1 → 6.5.10

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