sidekiq 5.1.1 → 6.5.9

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 (126) hide show
  1. checksums.yaml +5 -5
  2. data/Changes.md +507 -1
  3. data/LICENSE +3 -3
  4. data/README.md +24 -35
  5. data/bin/sidekiq +27 -3
  6. data/bin/sidekiqload +80 -68
  7. data/bin/sidekiqmon +8 -0
  8. data/lib/generators/sidekiq/job_generator.rb +57 -0
  9. data/lib/generators/sidekiq/templates/{worker.rb.erb → job.rb.erb} +2 -2
  10. data/lib/generators/sidekiq/templates/{worker_spec.rb.erb → job_spec.rb.erb} +1 -1
  11. data/lib/generators/sidekiq/templates/{worker_test.rb.erb → job_test.rb.erb} +1 -1
  12. data/lib/sidekiq/api.rb +537 -286
  13. data/lib/sidekiq/cli.rb +243 -240
  14. data/lib/sidekiq/client.rb +82 -85
  15. data/lib/sidekiq/component.rb +65 -0
  16. data/lib/sidekiq/delay.rb +9 -7
  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 +7 -5
  21. data/lib/sidekiq/fetch.rb +50 -40
  22. data/lib/sidekiq/job.rb +13 -0
  23. data/lib/sidekiq/job_logger.rb +36 -9
  24. data/lib/sidekiq/job_retry.rb +143 -97
  25. data/lib/sidekiq/job_util.rb +71 -0
  26. data/lib/sidekiq/launcher.rb +185 -85
  27. data/lib/sidekiq/logger.rb +156 -0
  28. data/lib/sidekiq/manager.rb +41 -43
  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 +28 -16
  39. data/lib/sidekiq/processor.rb +156 -98
  40. data/lib/sidekiq/rails.rb +48 -42
  41. data/lib/sidekiq/redis_client_adapter.rb +154 -0
  42. data/lib/sidekiq/redis_connection.rb +109 -51
  43. data/lib/sidekiq/ring_buffer.rb +29 -0
  44. data/lib/sidekiq/scheduled.rb +133 -41
  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 +72 -62
  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 -11
  52. data/lib/sidekiq/web/application.rb +127 -76
  53. data/lib/sidekiq/web/csrf_protection.rb +180 -0
  54. data/lib/sidekiq/web/helpers.rb +133 -96
  55. data/lib/sidekiq/web/router.rb +23 -19
  56. data/lib/sidekiq/web.rb +69 -109
  57. data/lib/sidekiq/worker.rb +268 -102
  58. data/lib/sidekiq.rb +175 -66
  59. data/sidekiq.gemspec +23 -23
  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 +65 -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 +0 -4
  69. data/web/assets/stylesheets/application.css +120 -232
  70. data/web/assets/stylesheets/bootstrap.css +2 -2
  71. data/web/locales/ar.yml +9 -2
  72. data/web/locales/de.yml +14 -2
  73. data/web/locales/el.yml +43 -19
  74. data/web/locales/en.yml +14 -1
  75. data/web/locales/es.yml +21 -5
  76. data/web/locales/fr.yml +10 -3
  77. data/web/locales/ja.yml +14 -1
  78. data/web/locales/lt.yml +83 -0
  79. data/web/locales/pl.yml +4 -4
  80. data/web/locales/pt-br.yml +27 -9
  81. data/web/locales/ru.yml +4 -0
  82. data/web/locales/vi.yml +83 -0
  83. data/web/locales/zh-cn.yml +36 -11
  84. data/web/locales/zh-tw.yml +32 -7
  85. data/web/views/_footer.erb +4 -1
  86. data/web/views/_job_info.erb +3 -2
  87. data/web/views/_nav.erb +4 -18
  88. data/web/views/_poll_link.erb +2 -5
  89. data/web/views/_summary.erb +7 -7
  90. data/web/views/busy.erb +61 -22
  91. data/web/views/dashboard.erb +23 -14
  92. data/web/views/dead.erb +3 -3
  93. data/web/views/layout.erb +4 -2
  94. data/web/views/metrics.erb +69 -0
  95. data/web/views/metrics_for_job.erb +87 -0
  96. data/web/views/morgue.erb +9 -6
  97. data/web/views/queue.erb +24 -10
  98. data/web/views/queues.erb +11 -3
  99. data/web/views/retries.erb +14 -7
  100. data/web/views/retry.erb +3 -3
  101. data/web/views/scheduled.erb +5 -2
  102. metadata +62 -135
  103. data/.github/contributing.md +0 -32
  104. data/.github/issue_template.md +0 -11
  105. data/.gitignore +0 -13
  106. data/.travis.yml +0 -14
  107. data/3.0-Upgrade.md +0 -70
  108. data/4.0-Upgrade.md +0 -53
  109. data/5.0-Upgrade.md +0 -56
  110. data/COMM-LICENSE +0 -95
  111. data/Ent-Changes.md +0 -210
  112. data/Gemfile +0 -8
  113. data/Pro-2.0-Upgrade.md +0 -138
  114. data/Pro-3.0-Upgrade.md +0 -44
  115. data/Pro-4.0-Upgrade.md +0 -35
  116. data/Pro-Changes.md +0 -716
  117. data/Rakefile +0 -8
  118. data/bin/sidekiqctl +0 -99
  119. data/code_of_conduct.md +0 -50
  120. data/lib/generators/sidekiq/worker_generator.rb +0 -49
  121. data/lib/sidekiq/core_ext.rb +0 -1
  122. data/lib/sidekiq/exception_handler.rb +0 -29
  123. data/lib/sidekiq/logging.rb +0 -122
  124. data/lib/sidekiq/middleware/server/active_record.rb +0 -22
  125. data/lib/sidekiq/middleware/server/active_record_cache.rb +0 -11
  126. data/lib/sidekiq/util.rb +0 -66
@@ -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,107 +68,200 @@ module Sidekiq
61
68
 
62
69
  private unless $TESTING
63
70
 
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
96
+
64
97
  def heartbeat
65
- results = Sidekiq::CLI::PROCTITLES.map {|x| x.(self, to_data) }
66
- results.compact!
67
- $0 = results.join(' ')
98
+ $0 = PROCTITLES.map { |proc| proc.call(self, to_data) }.compact.join(" ")
68
99
 
69
100
 
70
101
  end
71
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
+
72
128
  def ❤
73
129
  key = identity
74
130
  fails = procd = 0
131
+
75
132
  begin
76
- Processor::FAILURE.update {|curr| fails = curr; 0 }
77
- Processor::PROCESSED.update {|curr| procd = curr; 0 }
133
+ fails = Processor::FAILURE.reset
134
+ procd = Processor::PROCESSED.reset
135
+ curstate = Processor::WORK_STATE.dup
78
136
 
79
- workers_key = "#{key}:workers".freeze
80
- nowdate = Time.now.utc.strftime("%Y-%m-%d".freeze)
81
- Sidekiq.redis do |conn|
82
- conn.multi do
83
- conn.incrby("stat:processed".freeze, procd)
84
- conn.incrby("stat:processed:#{nowdate}", procd)
85
- conn.incrby("stat:failed".freeze, fails)
86
- conn.incrby("stat:failed:#{nowdate}", fails)
87
- conn.del(workers_key)
88
- Processor::WORKER_STATE.each_pair do |tid, hash|
89
- 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))
90
156
  end
91
- conn.expire(workers_key, 60)
157
+ transaction.expire(work_key, 60)
92
158
  end
93
159
  end
160
+
161
+ rtt = check_rtt
162
+
94
163
  fails = procd = 0
164
+ kb = memory_usage(::Process.pid)
95
165
 
96
- _, exists, _, _, msg = Sidekiq.redis do |conn|
97
- conn.multi do
98
- conn.sadd('processes', key)
99
- conn.exists(key)
100
- conn.hmset(key, 'info', to_json, 'busy', Processor::WORKER_STATE.size, 'beat', Time.now.to_f, 'quiet', @done)
101
- conn.expire(key, 60)
102
- conn.rpop("#{key}-signals")
103
- end
104
- 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
+ }
105
180
 
106
181
  # first heartbeat or recovering from an outage and need to reestablish our heartbeat
107
- fire_event(:heartbeat) if !exists
182
+ fire_event(:heartbeat) unless exists
183
+ fire_event(:beat, oneshot: false)
108
184
 
109
185
  return unless msg
110
186
 
111
- ::Process.kill(msg, $$)
187
+ ::Process.kill(msg, ::Process.pid)
112
188
  rescue => e
113
189
  # ignore all redis/network issues
114
- logger.error("heartbeat: #{e.message}")
190
+ logger.error("heartbeat: #{e}")
115
191
  # don't lose the counts if there was a network issue
116
- Processor::PROCESSED.increment(procd)
117
- Processor::FAILURE.increment(fails)
192
+ Processor::PROCESSED.incr(procd)
193
+ Processor::FAILURE.incr(fails)
118
194
  end
119
195
  end
120
196
 
121
- def start_heartbeat
122
- while true
123
- heartbeat
124
- 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)
125
209
  end
126
- 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
127
226
  end
128
227
 
129
- def to_data
130
- @data ||= begin
131
- {
132
- 'hostname' => hostname,
133
- 'started_at' => Time.now.to_f,
134
- 'pid' => $$,
135
- 'tag' => @options[:tag] || '',
136
- 'concurrency' => @options[:concurrency],
137
- 'queues' => @options[:queues].uniq,
138
- 'labels' => @options[:labels],
139
- 'identity' => identity,
140
- }
141
- 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 }
142
242
  end
143
243
 
144
- def to_json
145
- @json ||= begin
146
- # this data changes infrequently so dump it to a string
147
- # now so we don't need to dump it every heartbeat.
148
- Sidekiq.dump_json(to_data)
149
- end
244
+ def memory_usage(pid)
245
+ MEMORY_GRABBER.call(pid)
150
246
  end
151
247
 
152
- def clear_heartbeat
153
- # Remove record from Redis since we are shutting down.
154
- # Note we don't stop the heartbeat thread; if the process
155
- # doesn't actually exit, it'll reappear in the Web UI.
156
- Sidekiq.redis do |conn|
157
- conn.pipelined do
158
- conn.srem('processes', identity)
159
- conn.del("#{identity}:workers")
160
- end
161
- end
162
- rescue
163
- # 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
+ }
164
259
  end
165
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
166
266
  end
167
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,43 +20,37 @@ 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 }
49
+ logger.info { "Terminating quiet threads" }
50
+ @workers.each(&:terminate)
57
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
56
  fire_event(:shutdown, reverse: true)
@@ -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