sidekiq 6.1.2 → 6.5.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of sidekiq might be problematic. Click here for more details.

Files changed (113) hide show
  1. checksums.yaml +4 -4
  2. data/Changes.md +215 -2
  3. data/LICENSE +3 -3
  4. data/README.md +9 -4
  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 +321 -145
  13. data/lib/sidekiq/cli.rb +73 -40
  14. data/lib/sidekiq/client.rb +48 -72
  15. data/lib/sidekiq/{util.rb → component.rb} +12 -14
  16. data/lib/sidekiq/delay.rb +3 -1
  17. data/lib/sidekiq/extensions/generic_proxy.rb +4 -2
  18. data/lib/sidekiq/fetch.rb +31 -20
  19. data/lib/sidekiq/job.rb +13 -0
  20. data/lib/sidekiq/job_logger.rb +16 -28
  21. data/lib/sidekiq/job_retry.rb +79 -59
  22. data/lib/sidekiq/job_util.rb +71 -0
  23. data/lib/sidekiq/launcher.rb +126 -65
  24. data/lib/sidekiq/logger.rb +11 -20
  25. data/lib/sidekiq/manager.rb +35 -34
  26. data/lib/sidekiq/metrics/deploy.rb +47 -0
  27. data/lib/sidekiq/metrics/query.rb +153 -0
  28. data/lib/sidekiq/metrics/shared.rb +94 -0
  29. data/lib/sidekiq/metrics/tracking.rb +134 -0
  30. data/lib/sidekiq/middleware/chain.rb +87 -41
  31. data/lib/sidekiq/middleware/current_attributes.rb +63 -0
  32. data/lib/sidekiq/middleware/i18n.rb +6 -4
  33. data/lib/sidekiq/middleware/modules.rb +21 -0
  34. data/lib/sidekiq/monitor.rb +1 -1
  35. data/lib/sidekiq/paginator.rb +8 -8
  36. data/lib/sidekiq/processor.rb +47 -41
  37. data/lib/sidekiq/rails.rb +22 -4
  38. data/lib/sidekiq/redis_client_adapter.rb +154 -0
  39. data/lib/sidekiq/redis_connection.rb +84 -55
  40. data/lib/sidekiq/ring_buffer.rb +29 -0
  41. data/lib/sidekiq/scheduled.rb +55 -25
  42. data/lib/sidekiq/testing/inline.rb +4 -4
  43. data/lib/sidekiq/testing.rb +38 -39
  44. data/lib/sidekiq/transaction_aware_client.rb +45 -0
  45. data/lib/sidekiq/version.rb +1 -1
  46. data/lib/sidekiq/web/action.rb +3 -3
  47. data/lib/sidekiq/web/application.rb +37 -13
  48. data/lib/sidekiq/web/csrf_protection.rb +30 -8
  49. data/lib/sidekiq/web/helpers.rb +60 -28
  50. data/lib/sidekiq/web/router.rb +4 -1
  51. data/lib/sidekiq/web.rb +38 -78
  52. data/lib/sidekiq/worker.rb +136 -13
  53. data/lib/sidekiq.rb +114 -31
  54. data/sidekiq.gemspec +12 -4
  55. data/web/assets/images/apple-touch-icon.png +0 -0
  56. data/web/assets/javascripts/application.js +113 -60
  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.js +50 -67
  60. data/web/assets/javascripts/graph.js +16 -0
  61. data/web/assets/javascripts/metrics.js +262 -0
  62. data/web/assets/stylesheets/application-dark.css +36 -36
  63. data/web/assets/stylesheets/application-rtl.css +0 -4
  64. data/web/assets/stylesheets/application.css +82 -237
  65. data/web/locales/ar.yml +8 -2
  66. data/web/locales/el.yml +43 -19
  67. data/web/locales/en.yml +11 -1
  68. data/web/locales/es.yml +18 -2
  69. data/web/locales/fr.yml +8 -1
  70. data/web/locales/ja.yml +3 -0
  71. data/web/locales/lt.yml +1 -1
  72. data/web/locales/pt-br.yml +27 -9
  73. data/web/views/_footer.erb +1 -1
  74. data/web/views/_job_info.erb +1 -1
  75. data/web/views/_nav.erb +1 -1
  76. data/web/views/_poll_link.erb +2 -5
  77. data/web/views/_summary.erb +7 -7
  78. data/web/views/busy.erb +50 -19
  79. data/web/views/dashboard.erb +23 -14
  80. data/web/views/dead.erb +1 -1
  81. data/web/views/layout.erb +2 -1
  82. data/web/views/metrics.erb +69 -0
  83. data/web/views/metrics_for_job.erb +87 -0
  84. data/web/views/morgue.erb +6 -6
  85. data/web/views/queue.erb +15 -11
  86. data/web/views/queues.erb +3 -3
  87. data/web/views/retries.erb +7 -7
  88. data/web/views/retry.erb +1 -1
  89. data/web/views/scheduled.erb +1 -1
  90. metadata +43 -36
  91. data/.github/ISSUE_TEMPLATE/bug_report.md +0 -20
  92. data/.github/contributing.md +0 -32
  93. data/.github/workflows/ci.yml +0 -41
  94. data/.gitignore +0 -13
  95. data/.standard.yml +0 -20
  96. data/3.0-Upgrade.md +0 -70
  97. data/4.0-Upgrade.md +0 -53
  98. data/5.0-Upgrade.md +0 -56
  99. data/6.0-Upgrade.md +0 -72
  100. data/COMM-LICENSE +0 -97
  101. data/Ent-2.0-Upgrade.md +0 -37
  102. data/Ent-Changes.md +0 -281
  103. data/Gemfile +0 -24
  104. data/Gemfile.lock +0 -192
  105. data/Pro-2.0-Upgrade.md +0 -138
  106. data/Pro-3.0-Upgrade.md +0 -44
  107. data/Pro-4.0-Upgrade.md +0 -35
  108. data/Pro-5.0-Upgrade.md +0 -25
  109. data/Pro-Changes.md +0 -805
  110. data/Rakefile +0 -10
  111. data/code_of_conduct.md +0 -50
  112. data/lib/generators/sidekiq/worker_generator.rb +0 -57
  113. 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