sidekiq 4.2.10 → 6.5.7

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 (131) hide show
  1. checksums.yaml +5 -5
  2. data/Changes.md +573 -1
  3. data/LICENSE +3 -3
  4. data/README.md +25 -34
  5. data/bin/sidekiq +27 -3
  6. data/bin/sidekiqload +81 -74
  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/job_spec.rb.erb +6 -0
  11. data/lib/generators/sidekiq/templates/{worker_test.rb.erb → job_test.rb.erb} +1 -1
  12. data/lib/sidekiq/api.rb +585 -285
  13. data/lib/sidekiq/cli.rb +256 -233
  14. data/lib/sidekiq/client.rb +86 -83
  15. data/lib/sidekiq/component.rb +65 -0
  16. data/lib/sidekiq/delay.rb +43 -0
  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 +13 -5
  21. data/lib/sidekiq/fetch.rb +50 -40
  22. data/lib/sidekiq/job.rb +13 -0
  23. data/lib/sidekiq/job_logger.rb +51 -0
  24. data/lib/sidekiq/job_retry.rb +282 -0
  25. data/lib/sidekiq/job_util.rb +71 -0
  26. data/lib/sidekiq/launcher.rb +184 -90
  27. data/lib/sidekiq/logger.rb +156 -0
  28. data/lib/sidekiq/manager.rb +43 -45
  29. data/lib/sidekiq/metrics/deploy.rb +47 -0
  30. data/lib/sidekiq/metrics/query.rb +153 -0
  31. data/lib/sidekiq/metrics/shared.rb +94 -0
  32. data/lib/sidekiq/metrics/tracking.rb +134 -0
  33. data/lib/sidekiq/middleware/chain.rb +102 -46
  34. data/lib/sidekiq/middleware/current_attributes.rb +63 -0
  35. data/lib/sidekiq/middleware/i18n.rb +7 -7
  36. data/lib/sidekiq/middleware/modules.rb +21 -0
  37. data/lib/sidekiq/monitor.rb +133 -0
  38. data/lib/sidekiq/paginator.rb +20 -16
  39. data/lib/sidekiq/processor.rb +176 -91
  40. data/lib/sidekiq/rails.rb +41 -96
  41. data/lib/sidekiq/redis_client_adapter.rb +154 -0
  42. data/lib/sidekiq/redis_connection.rb +117 -48
  43. data/lib/sidekiq/ring_buffer.rb +29 -0
  44. data/lib/sidekiq/scheduled.rb +134 -44
  45. data/lib/sidekiq/sd_notify.rb +149 -0
  46. data/lib/sidekiq/systemd.rb +24 -0
  47. data/lib/sidekiq/testing/inline.rb +6 -5
  48. data/lib/sidekiq/testing.rb +80 -61
  49. data/lib/sidekiq/transaction_aware_client.rb +45 -0
  50. data/lib/sidekiq/version.rb +2 -1
  51. data/lib/sidekiq/web/action.rb +15 -15
  52. data/lib/sidekiq/web/application.rb +129 -86
  53. data/lib/sidekiq/web/csrf_protection.rb +180 -0
  54. data/lib/sidekiq/web/helpers.rb +170 -83
  55. data/lib/sidekiq/web/router.rb +23 -19
  56. data/lib/sidekiq/web.rb +69 -109
  57. data/lib/sidekiq/worker.rb +290 -41
  58. data/lib/sidekiq.rb +185 -77
  59. data/sidekiq.gemspec +23 -27
  60. data/web/assets/images/apple-touch-icon.png +0 -0
  61. data/web/assets/javascripts/application.js +112 -61
  62. data/web/assets/javascripts/chart.min.js +13 -0
  63. data/web/assets/javascripts/chartjs-plugin-annotation.min.js +7 -0
  64. data/web/assets/javascripts/dashboard.js +70 -91
  65. data/web/assets/javascripts/graph.js +16 -0
  66. data/web/assets/javascripts/metrics.js +262 -0
  67. data/web/assets/stylesheets/application-dark.css +143 -0
  68. data/web/assets/stylesheets/application-rtl.css +242 -0
  69. data/web/assets/stylesheets/application.css +364 -144
  70. data/web/assets/stylesheets/bootstrap-rtl.min.css +9 -0
  71. data/web/assets/stylesheets/bootstrap.css +2 -2
  72. data/web/locales/ar.yml +87 -0
  73. data/web/locales/de.yml +14 -2
  74. data/web/locales/el.yml +43 -19
  75. data/web/locales/en.yml +15 -1
  76. data/web/locales/es.yml +22 -5
  77. data/web/locales/fa.yml +1 -0
  78. data/web/locales/fr.yml +10 -3
  79. data/web/locales/he.yml +79 -0
  80. data/web/locales/ja.yml +19 -4
  81. data/web/locales/lt.yml +83 -0
  82. data/web/locales/pl.yml +4 -4
  83. data/web/locales/pt-br.yml +27 -9
  84. data/web/locales/ru.yml +4 -0
  85. data/web/locales/ur.yml +80 -0
  86. data/web/locales/vi.yml +83 -0
  87. data/web/locales/zh-cn.yml +36 -11
  88. data/web/locales/zh-tw.yml +32 -7
  89. data/web/views/_footer.erb +5 -2
  90. data/web/views/_job_info.erb +3 -2
  91. data/web/views/_nav.erb +5 -19
  92. data/web/views/_paging.erb +1 -1
  93. data/web/views/_poll_link.erb +2 -5
  94. data/web/views/_summary.erb +7 -7
  95. data/web/views/busy.erb +62 -24
  96. data/web/views/dashboard.erb +24 -15
  97. data/web/views/dead.erb +3 -3
  98. data/web/views/layout.erb +14 -3
  99. data/web/views/metrics.erb +69 -0
  100. data/web/views/metrics_for_job.erb +87 -0
  101. data/web/views/morgue.erb +9 -6
  102. data/web/views/queue.erb +26 -12
  103. data/web/views/queues.erb +12 -2
  104. data/web/views/retries.erb +14 -7
  105. data/web/views/retry.erb +3 -3
  106. data/web/views/scheduled.erb +7 -4
  107. metadata +66 -206
  108. data/.github/contributing.md +0 -32
  109. data/.github/issue_template.md +0 -9
  110. data/.gitignore +0 -12
  111. data/.travis.yml +0 -18
  112. data/3.0-Upgrade.md +0 -70
  113. data/4.0-Upgrade.md +0 -53
  114. data/COMM-LICENSE +0 -95
  115. data/Ent-Changes.md +0 -173
  116. data/Gemfile +0 -29
  117. data/Pro-2.0-Upgrade.md +0 -138
  118. data/Pro-3.0-Upgrade.md +0 -44
  119. data/Pro-Changes.md +0 -628
  120. data/Rakefile +0 -12
  121. data/bin/sidekiqctl +0 -99
  122. data/code_of_conduct.md +0 -50
  123. data/lib/generators/sidekiq/templates/worker_spec.rb.erb +0 -6
  124. data/lib/generators/sidekiq/worker_generator.rb +0 -49
  125. data/lib/sidekiq/core_ext.rb +0 -119
  126. data/lib/sidekiq/exception_handler.rb +0 -31
  127. data/lib/sidekiq/logging.rb +0 -106
  128. data/lib/sidekiq/middleware/server/active_record.rb +0 -13
  129. data/lib/sidekiq/middleware/server/logging.rb +0 -31
  130. data/lib/sidekiq/middleware/server/retry_jobs.rb +0 -205
  131. data/lib/sidekiq/util.rb +0 -63
@@ -1,24 +1,33 @@
1
- # encoding: utf-8
2
1
  # frozen_string_literal: true
3
- require 'sidekiq/manager'
4
- require 'sidekiq/fetch'
5
- require 'sidekiq/scheduled'
2
+
3
+ require "sidekiq/manager"
4
+ require "sidekiq/fetch"
5
+ require "sidekiq/scheduled"
6
+ require "sidekiq/ring_buffer"
6
7
 
7
8
  module Sidekiq
8
- # The Launcher is a very simple Actor whose job is to
9
- # start, monitor and stop the core Actors in Sidekiq.
10
- # If any of these actors die, the Sidekiq process exits
11
- # immediately.
9
+ # The Launcher starts the Manager and Poller threads and provides the process heartbeat.
12
10
  class Launcher
13
- include Util
11
+ include Sidekiq::Component
12
+
13
+ STATS_TTL = 5 * 365 * 24 * 60 * 60 # 5 years
14
+
15
+ PROCTITLES = [
16
+ proc { "sidekiq" },
17
+ proc { Sidekiq::VERSION },
18
+ proc { |me, data| data["tag"] },
19
+ proc { |me, data| "[#{Processor::WORK_STATE.size} of #{data["concurrency"]} busy]" },
20
+ proc { |me, data| "stopping" if me.stopping? }
21
+ ]
14
22
 
15
23
  attr_accessor :manager, :poller, :fetcher
16
24
 
17
25
  def initialize(options)
26
+ @config = options
27
+ options[:fetch] ||= BasicFetch.new(options)
18
28
  @manager = Sidekiq::Manager.new(options)
19
- @poller = Sidekiq::Scheduled::Poller.new
29
+ @poller = Sidekiq::Scheduled::Poller.new(options)
20
30
  @done = false
21
- @options = options
22
31
  end
23
32
 
24
33
  def run
@@ -35,11 +44,9 @@ module Sidekiq
35
44
  @poller.terminate
36
45
  end
37
46
 
38
- # Shuts down the process. This method does not
39
- # return until all work is complete and cleaned up.
40
- # 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.
41
48
  def stop
42
- deadline = Time.now + @options[:timeout]
49
+ deadline = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC) + @config[:timeout]
43
50
 
44
51
  @done = true
45
52
  @manager.quiet
@@ -47,10 +54,10 @@ module Sidekiq
47
54
 
48
55
  @manager.stop(deadline)
49
56
 
50
- # 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.
51
58
  # This call is a no-op in Sidekiq but necessary for Sidekiq Pro.
52
- strategy = (@options[:fetch] || Sidekiq::BasicFetch)
53
- strategy.bulk_requeue([], @options)
59
+ strategy = @config[:fetch]
60
+ strategy.bulk_requeue([], @config)
54
61
 
55
62
  clear_heartbeat
56
63
  end
@@ -61,113 +68,200 @@ module Sidekiq
61
68
 
62
69
  private unless $TESTING
63
70
 
64
- JVM_RESERVED_SIGNALS = ['USR1', 'USR2'] # Don't Process#kill if we get these signals via the API
71
+ BEAT_PAUSE = 5
72
+
73
+ def start_heartbeat
74
+ loop do
75
+ heartbeat
76
+ sleep BEAT_PAUSE
77
+ end
78
+ logger.info("Heartbeat stopping...")
79
+ end
80
+
81
+ def clear_heartbeat
82
+ flush_stats
83
+
84
+ # Remove record from Redis since we are shutting down.
85
+ # Note we don't stop the heartbeat thread; if the process
86
+ # doesn't actually exit, it'll reappear in the Web UI.
87
+ redis do |conn|
88
+ conn.pipelined do |pipeline|
89
+ pipeline.srem("processes", [identity])
90
+ pipeline.unlink("#{identity}:work")
91
+ end
92
+ end
93
+ rescue
94
+ # best effort, ignore network errors
95
+ end
65
96
 
66
97
  def heartbeat
67
- results = Sidekiq::CLI::PROCTITLES.map {|x| x.(self, to_data) }
68
- results.compact!
69
- $0 = results.join(' ')
98
+ $0 = PROCTITLES.map { |proc| proc.call(self, to_data) }.compact.join(" ")
70
99
 
71
100
 
72
101
  end
73
102
 
103
+ def flush_stats
104
+ fails = Processor::FAILURE.reset
105
+ procd = Processor::PROCESSED.reset
106
+ return if fails + procd == 0
107
+
108
+ nowdate = Time.now.utc.strftime("%Y-%m-%d")
109
+ begin
110
+ Sidekiq.redis do |conn|
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)
119
+ end
120
+ end
121
+ rescue => ex
122
+ # we're exiting the process, things might be shut down so don't
123
+ # try to handle the exception
124
+ Sidekiq.logger.warn("Unable to flush stats: #{ex}")
125
+ end
126
+ end
127
+
74
128
  def ❤
75
129
  key = identity
76
130
  fails = procd = 0
131
+
77
132
  begin
78
- Processor::FAILURE.update {|curr| fails = curr; 0 }
79
- Processor::PROCESSED.update {|curr| procd = curr; 0 }
133
+ fails = Processor::FAILURE.reset
134
+ procd = Processor::PROCESSED.reset
135
+ curstate = Processor::WORK_STATE.dup
80
136
 
81
- workers_key = "#{key}:workers".freeze
82
- nowdate = Time.now.utc.strftime("%Y-%m-%d".freeze)
83
- Sidekiq.redis do |conn|
84
- conn.multi do
85
- conn.incrby("stat:processed".freeze, procd)
86
- conn.incrby("stat:processed:#{nowdate}", procd)
87
- conn.incrby("stat:failed".freeze, fails)
88
- conn.incrby("stat:failed:#{nowdate}", fails)
89
- conn.del(workers_key)
90
- Processor::WORKER_STATE.each_pair do |tid, hash|
91
- conn.hset(workers_key, tid, Sidekiq.dump_json(hash))
137
+ nowdate = Time.now.utc.strftime("%Y-%m-%d")
138
+
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)
144
+
145
+ transaction.incrby("stat:failed", fails)
146
+ transaction.incrby("stat:failed:#{nowdate}", fails)
147
+ transaction.expire("stat:failed:#{nowdate}", STATS_TTL)
148
+ end
149
+
150
+ # work is the current set of executing jobs
151
+ work_key = "#{key}:work"
152
+ conn.pipelined do |transaction|
153
+ transaction.unlink(work_key)
154
+ curstate.each_pair do |tid, hash|
155
+ transaction.hset(work_key, tid, Sidekiq.dump_json(hash))
92
156
  end
93
- conn.expire(workers_key, 60)
157
+ transaction.expire(work_key, 60)
94
158
  end
95
159
  end
160
+
161
+ rtt = check_rtt
162
+
96
163
  fails = procd = 0
164
+ kb = memory_usage(::Process.pid)
97
165
 
98
- _, exists, _, _, msg = Sidekiq.redis do |conn|
99
- conn.multi do
100
- conn.sadd('processes', key)
101
- conn.exists(key)
102
- conn.hmset(key, 'info', to_json, 'busy', Processor::WORKER_STATE.size, 'beat', Time.now.to_f, 'quiet', @done)
103
- conn.expire(key, 60)
104
- conn.rpop("#{key}-signals")
105
- end
106
- end
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")
178
+ }
179
+ }
107
180
 
108
181
  # first heartbeat or recovering from an outage and need to reestablish our heartbeat
109
- fire_event(:heartbeat) if !exists
182
+ fire_event(:heartbeat) unless exists
183
+ fire_event(:beat, oneshot: false)
110
184
 
111
185
  return unless msg
112
186
 
113
- if JVM_RESERVED_SIGNALS.include?(msg)
114
- Sidekiq::CLI.instance.handle_signal(msg)
115
- else
116
- ::Process.kill(msg, $$)
117
- end
187
+ ::Process.kill(msg, ::Process.pid)
118
188
  rescue => e
119
189
  # ignore all redis/network issues
120
- logger.error("heartbeat: #{e.message}")
190
+ logger.error("heartbeat: #{e}")
121
191
  # don't lose the counts if there was a network issue
122
- Processor::PROCESSED.increment(procd)
123
- Processor::FAILURE.increment(fails)
192
+ Processor::PROCESSED.incr(procd)
193
+ Processor::FAILURE.incr(fails)
124
194
  end
125
195
  end
126
196
 
127
- def start_heartbeat
128
- while true
129
- heartbeat
130
- sleep 5
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)
131
209
  end
132
- Sidekiq.logger.info("Heartbeat stopping...")
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
224
+ end
225
+ rtt
133
226
  end
134
227
 
135
- def to_data
136
- @data ||= begin
137
- {
138
- 'hostname' => hostname,
139
- 'started_at' => Time.now.to_f,
140
- 'pid' => $$,
141
- 'tag' => @options[:tag] || '',
142
- 'concurrency' => @options[:concurrency],
143
- 'queues' => @options[:queues].uniq,
144
- 'labels' => @options[:labels],
145
- 'identity' => identity,
146
- }
147
- end
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 }
148
242
  end
149
243
 
150
- def to_json
151
- @json ||= begin
152
- # this data changes infrequently so dump it to a string
153
- # now so we don't need to dump it every heartbeat.
154
- Sidekiq.dump_json(to_data)
155
- end
244
+ def memory_usage(pid)
245
+ MEMORY_GRABBER.call(pid)
156
246
  end
157
247
 
158
- def clear_heartbeat
159
- # Remove record from Redis since we are shutting down.
160
- # Note we don't stop the heartbeat thread; if the process
161
- # doesn't actually exit, it'll reappear in the Web UI.
162
- Sidekiq.redis do |conn|
163
- conn.pipelined do
164
- conn.srem('processes', identity)
165
- conn.del("#{identity}:workers")
166
- end
167
- end
168
- rescue
169
- # best effort, ignore network errors
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
+ }
170
259
  end
171
260
 
261
+ def to_json
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)
265
+ end
172
266
  end
173
267
  end
@@ -0,0 +1,156 @@
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
+ current[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
+ LEVELS.each do |level, numeric_level|
39
+ define_method("#{level}?") do
40
+ local_level.nil? ? super() : local_level <= numeric_level
41
+ end
42
+ end
43
+
44
+ def local_level
45
+ Thread.current[:sidekiq_log_level]
46
+ end
47
+
48
+ def local_level=(level)
49
+ case level
50
+ when Integer
51
+ Thread.current[:sidekiq_log_level] = level
52
+ when Symbol, String
53
+ Thread.current[:sidekiq_log_level] = LEVELS[level.to_s]
54
+ when nil
55
+ Thread.current[:sidekiq_log_level] = nil
56
+ else
57
+ raise ArgumentError, "Invalid log level: #{level.inspect}"
58
+ end
59
+ end
60
+
61
+ def level
62
+ local_level || super
63
+ end
64
+
65
+ # Change the thread-local level for the duration of the given block.
66
+ def log_at(level)
67
+ old_local_level = local_level
68
+ self.local_level = level
69
+ yield
70
+ ensure
71
+ self.local_level = old_local_level
72
+ end
73
+
74
+ # Redefined to check severity against #level, and thus the thread-local level, rather than +@level+.
75
+ # FIXME: Remove when the minimum Ruby version supports overriding Logger#level.
76
+ def add(severity, message = nil, progname = nil, &block)
77
+ severity ||= ::Logger::UNKNOWN
78
+ progname ||= @progname
79
+
80
+ return true if @logdev.nil? || severity < level
81
+
82
+ if message.nil?
83
+ if block
84
+ message = yield
85
+ else
86
+ message = progname
87
+ progname = @progname
88
+ end
89
+ end
90
+
91
+ @logdev.write format_message(format_severity(severity), Time.now, progname, message)
92
+ end
93
+ end
94
+
95
+ class Logger < ::Logger
96
+ include LoggingUtils
97
+
98
+ def initialize(*args, **kwargs)
99
+ super
100
+ self.formatter = Sidekiq.log_formatter
101
+ end
102
+
103
+ module Formatters
104
+ class Base < ::Logger::Formatter
105
+ def tid
106
+ Thread.current["sidekiq_tid"] ||= (Thread.current.object_id ^ ::Process.pid).to_s(36)
107
+ end
108
+
109
+ def ctx
110
+ Sidekiq::Context.current
111
+ end
112
+
113
+ def format_context
114
+ if ctx.any?
115
+ " " + ctx.compact.map { |k, v|
116
+ case v
117
+ when Array
118
+ "#{k}=#{v.join(",")}"
119
+ else
120
+ "#{k}=#{v}"
121
+ end
122
+ }.join(" ")
123
+ end
124
+ end
125
+ end
126
+
127
+ class Pretty < Base
128
+ def call(severity, time, program_name, message)
129
+ "#{time.utc.iso8601(3)} pid=#{::Process.pid} tid=#{tid}#{format_context} #{severity}: #{message}\n"
130
+ end
131
+ end
132
+
133
+ class WithoutTimestamp < Pretty
134
+ def call(severity, time, program_name, message)
135
+ "pid=#{::Process.pid} tid=#{tid}#{format_context} #{severity}: #{message}\n"
136
+ end
137
+ end
138
+
139
+ class JSON < Base
140
+ def call(severity, time, program_name, message)
141
+ hash = {
142
+ ts: time.utc.iso8601(3),
143
+ pid: ::Process.pid,
144
+ tid: tid,
145
+ lvl: severity,
146
+ msg: message
147
+ }
148
+ c = ctx
149
+ hash["ctx"] = c unless c.empty?
150
+
151
+ Sidekiq.dump_json(hash) << "\n"
152
+ end
153
+ end
154
+ end
155
+ end
156
+ end
@@ -1,13 +1,10 @@
1
- # encoding: utf-8
2
1
  # frozen_string_literal: true
3
- require 'sidekiq/util'
4
- require 'sidekiq/processor'
5
- require 'sidekiq/fetch'
6
- require 'thread'
7
- require 'set'
8
2
 
9
- module Sidekiq
3
+ require "sidekiq/processor"
4
+ require "sidekiq/fetch"
5
+ require "set"
10
6
 
7
+ module Sidekiq
11
8
  ##
12
9
  # The Manager is the central coordination point in Sidekiq, controlling
13
10
  # the lifecycle of the Processors.
@@ -23,46 +20,40 @@ module Sidekiq
23
20
  # the shutdown process. The other tasks are performed by other threads.
24
21
  #
25
22
  class Manager
26
- include Util
23
+ include Sidekiq::Component
27
24
 
28
25
  attr_reader :workers
29
- attr_reader :options
30
26
 
31
- def initialize(options={})
27
+ def initialize(options = {})
28
+ @config = options
32
29
  logger.debug { options.inspect }
33
- @options = options
34
- @count = options[:concurrency] || 25
30
+ @count = options[:concurrency] || 10
35
31
  raise ArgumentError, "Concurrency of #{@count} is not supported" if @count < 1
36
32
 
37
33
  @done = false
38
34
  @workers = Set.new
39
35
  @count.times do
40
- @workers << Processor.new(self)
36
+ @workers << Processor.new(@config, &method(:processor_result))
41
37
  end
42
38
  @plock = Mutex.new
43
39
  end
44
40
 
45
41
  def start
46
- @workers.each do |x|
47
- x.start
48
- end
42
+ @workers.each(&:start)
49
43
  end
50
44
 
51
45
  def quiet
52
46
  return if @done
53
47
  @done = true
54
48
 
55
- logger.info { "Terminating quiet workers" }
56
- @workers.each { |x| x.terminate }
57
- fire_event(:quiet, true)
49
+ logger.info { "Terminating quiet threads" }
50
+ @workers.each(&:terminate)
51
+ fire_event(:quiet, reverse: true)
58
52
  end
59
53
 
60
- # hack for quicker development / testing environment #2774
61
- PAUSE_TIME = STDOUT.tty? ? 0.1 : 0.5
62
-
63
54
  def stop(deadline)
64
55
  quiet
65
- fire_event(:shutdown, true)
56
+ fire_event(:shutdown, reverse: true)
66
57
 
67
58
  # some of the shutdown events can be async,
68
59
  # we don't have any way to know when they're done but
@@ -70,29 +61,18 @@ module Sidekiq
70
61
  sleep PAUSE_TIME
71
62
  return if @workers.empty?
72
63
 
73
- logger.info { "Pausing to allow workers to finish..." }
74
- remaining = deadline - Time.now
75
- while remaining > PAUSE_TIME
76
- return if @workers.empty?
77
- sleep PAUSE_TIME
78
- remaining = deadline - Time.now
79
- end
64
+ logger.info { "Pausing to allow jobs to finish..." }
65
+ wait_for(deadline) { @workers.empty? }
80
66
  return if @workers.empty?
81
67
 
82
68
  hard_shutdown
83
69
  end
84
70
 
85
- def processor_stopped(processor)
86
- @plock.synchronize do
87
- @workers.delete(processor)
88
- end
89
- end
90
-
91
- def processor_died(processor, reason)
71
+ def processor_result(processor, reason = nil)
92
72
  @plock.synchronize do
93
73
  @workers.delete(processor)
94
74
  unless @done
95
- p = Processor.new(self)
75
+ p = Processor.new(@config, &method(:processor_result))
96
76
  @workers << p
97
77
  p.start
98
78
  end
@@ -106,7 +86,7 @@ module Sidekiq
106
86
  private
107
87
 
108
88
  def hard_shutdown
109
- # We've reached the timeout and we still have busy workers.
89
+ # We've reached the timeout and we still have busy threads.
110
90
  # They must die but their jobs shall live on.
111
91
  cleanup = nil
112
92
  @plock.synchronize do
@@ -114,25 +94,43 @@ module Sidekiq
114
94
  end
115
95
 
116
96
  if cleanup.size > 0
117
- jobs = cleanup.map {|p| p.job }.compact
97
+ jobs = cleanup.map { |p| p.job }.compact
118
98
 
119
- logger.warn { "Terminating #{cleanup.size} busy worker threads" }
120
- 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}" }
121
101
 
122
102
  # Re-enqueue unfinished jobs
123
103
  # NOTE: You may notice that we may push a job back to redis before
124
- # the worker thread is terminated. This is ok because Sidekiq's
104
+ # the thread is terminated. This is ok because Sidekiq's
125
105
  # contract says that jobs are run AT LEAST once. Process termination
126
106
  # is delayed until we're certain the jobs are back in Redis because
127
107
  # it is worse to lose a job than to run it twice.
128
- strategy = (@options[:fetch] || Sidekiq::BasicFetch)
129
- strategy.bulk_requeue(jobs, @options)
108
+ strategy = @config[:fetch]
109
+ strategy.bulk_requeue(jobs, @config)
130
110
  end
131
111
 
132
112
  cleanup.each do |processor|
133
113
  processor.kill
134
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? }
135
121
  end
136
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
134
+ end
137
135
  end
138
136
  end