sidekiq_cleaner 5.3.6

Sign up to get free protection for your applications and to get access to all the features.
Files changed (122) hide show
  1. checksums.yaml +7 -0
  2. data/.circleci/config.yml +61 -0
  3. data/.github/contributing.md +32 -0
  4. data/.github/issue_template.md +11 -0
  5. data/.gitignore +15 -0
  6. data/.travis.yml +11 -0
  7. data/3.0-Upgrade.md +70 -0
  8. data/4.0-Upgrade.md +53 -0
  9. data/5.0-Upgrade.md +56 -0
  10. data/COMM-LICENSE +97 -0
  11. data/Changes.md +1536 -0
  12. data/Ent-Changes.md +238 -0
  13. data/Gemfile +23 -0
  14. data/LICENSE +9 -0
  15. data/Pro-2.0-Upgrade.md +138 -0
  16. data/Pro-3.0-Upgrade.md +44 -0
  17. data/Pro-4.0-Upgrade.md +35 -0
  18. data/Pro-Changes.md +759 -0
  19. data/README.md +55 -0
  20. data/Rakefile +9 -0
  21. data/bin/sidekiq +18 -0
  22. data/bin/sidekiqctl +20 -0
  23. data/bin/sidekiqload +149 -0
  24. data/cleaner/assets/images/favicon.ico +0 -0
  25. data/cleaner/assets/images/logo.png +0 -0
  26. data/cleaner/assets/images/status.png +0 -0
  27. data/cleaner/assets/javascripts/application.js +172 -0
  28. data/cleaner/assets/javascripts/dashboard.js +315 -0
  29. data/cleaner/assets/stylesheets/application-rtl.css +246 -0
  30. data/cleaner/assets/stylesheets/application.css +1144 -0
  31. data/cleaner/assets/stylesheets/bootstrap-rtl.min.css +9 -0
  32. data/cleaner/assets/stylesheets/bootstrap.css +5 -0
  33. data/cleaner/locales/ar.yml +81 -0
  34. data/cleaner/locales/cs.yml +78 -0
  35. data/cleaner/locales/da.yml +68 -0
  36. data/cleaner/locales/de.yml +69 -0
  37. data/cleaner/locales/el.yml +68 -0
  38. data/cleaner/locales/en.yml +81 -0
  39. data/cleaner/locales/es.yml +70 -0
  40. data/cleaner/locales/fa.yml +80 -0
  41. data/cleaner/locales/fr.yml +78 -0
  42. data/cleaner/locales/he.yml +79 -0
  43. data/cleaner/locales/hi.yml +75 -0
  44. data/cleaner/locales/it.yml +69 -0
  45. data/cleaner/locales/ja.yml +80 -0
  46. data/cleaner/locales/ko.yml +68 -0
  47. data/cleaner/locales/nb.yml +77 -0
  48. data/cleaner/locales/nl.yml +68 -0
  49. data/cleaner/locales/pl.yml +59 -0
  50. data/cleaner/locales/pt-br.yml +68 -0
  51. data/cleaner/locales/pt.yml +67 -0
  52. data/cleaner/locales/ru.yml +78 -0
  53. data/cleaner/locales/sv.yml +68 -0
  54. data/cleaner/locales/ta.yml +75 -0
  55. data/cleaner/locales/uk.yml +76 -0
  56. data/cleaner/locales/ur.yml +80 -0
  57. data/cleaner/locales/zh-cn.yml +68 -0
  58. data/cleaner/locales/zh-tw.yml +68 -0
  59. data/cleaner/views/_footer.erb +20 -0
  60. data/cleaner/views/_job_info.erb +88 -0
  61. data/cleaner/views/_nav.erb +52 -0
  62. data/cleaner/views/_paging.erb +23 -0
  63. data/cleaner/views/_poll_link.erb +7 -0
  64. data/cleaner/views/_status.erb +4 -0
  65. data/cleaner/views/_summary.erb +40 -0
  66. data/cleaner/views/busy.erb +98 -0
  67. data/cleaner/views/dashboard.erb +75 -0
  68. data/cleaner/views/dead.erb +34 -0
  69. data/cleaner/views/errors.erb +84 -0
  70. data/cleaner/views/layout.erb +40 -0
  71. data/cleaner/views/morgue.erb +75 -0
  72. data/cleaner/views/queue.erb +46 -0
  73. data/cleaner/views/queues.erb +30 -0
  74. data/cleaner/views/retries.erb +80 -0
  75. data/cleaner/views/retry.erb +34 -0
  76. data/cleaner/views/scheduled.erb +54 -0
  77. data/cleaner/views/scheduled_job_info.erb +8 -0
  78. data/cleaner-stats.png +0 -0
  79. data/cleaner.png +0 -0
  80. data/code_of_conduct.md +50 -0
  81. data/lib/generators/sidekiq/templates/worker.rb.erb +9 -0
  82. data/lib/generators/sidekiq/templates/worker_spec.rb.erb +6 -0
  83. data/lib/generators/sidekiq/templates/worker_test.rb.erb +8 -0
  84. data/lib/generators/sidekiq/worker_generator.rb +49 -0
  85. data/lib/sidekiq/api.rb +940 -0
  86. data/lib/sidekiq/cleaner/action.rb +89 -0
  87. data/lib/sidekiq/cleaner/application.rb +385 -0
  88. data/lib/sidekiq/cleaner/helpers.rb +325 -0
  89. data/lib/sidekiq/cleaner/router.rb +100 -0
  90. data/lib/sidekiq/cleaner.rb +214 -0
  91. data/lib/sidekiq/cli.rb +445 -0
  92. data/lib/sidekiq/client.rb +243 -0
  93. data/lib/sidekiq/core_ext.rb +1 -0
  94. data/lib/sidekiq/ctl.rb +221 -0
  95. data/lib/sidekiq/delay.rb +42 -0
  96. data/lib/sidekiq/exception_handler.rb +29 -0
  97. data/lib/sidekiq/extensions/action_mailer.rb +57 -0
  98. data/lib/sidekiq/extensions/active_record.rb +40 -0
  99. data/lib/sidekiq/extensions/class_methods.rb +40 -0
  100. data/lib/sidekiq/extensions/generic_proxy.rb +31 -0
  101. data/lib/sidekiq/fetch.rb +81 -0
  102. data/lib/sidekiq/job_logger.rb +25 -0
  103. data/lib/sidekiq/job_retry.rb +262 -0
  104. data/lib/sidekiq/launcher.rb +173 -0
  105. data/lib/sidekiq/logging.rb +122 -0
  106. data/lib/sidekiq/manager.rb +137 -0
  107. data/lib/sidekiq/middleware/chain.rb +150 -0
  108. data/lib/sidekiq/middleware/i18n.rb +42 -0
  109. data/lib/sidekiq/middleware/server/active_record.rb +23 -0
  110. data/lib/sidekiq/paginator.rb +43 -0
  111. data/lib/sidekiq/processor.rb +279 -0
  112. data/lib/sidekiq/rails.rb +58 -0
  113. data/lib/sidekiq/redis_connection.rb +144 -0
  114. data/lib/sidekiq/scheduled.rb +174 -0
  115. data/lib/sidekiq/testing/inline.rb +29 -0
  116. data/lib/sidekiq/testing.rb +333 -0
  117. data/lib/sidekiq/util.rb +66 -0
  118. data/lib/sidekiq/version.rb +4 -0
  119. data/lib/sidekiq/worker.rb +220 -0
  120. data/lib/sidekiq.rb +237 -0
  121. data/sidekiq_cleaner.gemspec +21 -0
  122. metadata +235 -0
@@ -0,0 +1,262 @@
1
+ # frozen_string_literal: true
2
+ require 'sidekiq/scheduled'
3
+ require 'sidekiq/api'
4
+
5
+ module Sidekiq
6
+ ##
7
+ # Automatically retry jobs that fail in Sidekiq.
8
+ # Sidekiq's retry support assumes a typical development lifecycle:
9
+ #
10
+ # 0. Push some code changes with a bug in it.
11
+ # 1. Bug causes job processing to fail, Sidekiq's middleware captures
12
+ # the job and pushes it onto a retry queue.
13
+ # 2. Sidekiq retries jobs in the retry queue multiple times with
14
+ # an exponential delay, the job continues to fail.
15
+ # 3. After a few days, a developer deploys a fix. The job is
16
+ # reprocessed successfully.
17
+ # 4. Once retries are exhausted, Sidekiq will give up and move the
18
+ # job to the Dead Job Queue (aka morgue) where it must be dealt with
19
+ # manually in the Web UI.
20
+ # 5. After 6 months on the DJQ, Sidekiq will discard the job.
21
+ #
22
+ # A job looks like:
23
+ #
24
+ # { 'class' => 'HardWorker', 'args' => [1, 2, 'foo'], 'retry' => true }
25
+ #
26
+ # The 'retry' option also accepts a number (in place of 'true'):
27
+ #
28
+ # { 'class' => 'HardWorker', 'args' => [1, 2, 'foo'], 'retry' => 5 }
29
+ #
30
+ # The job will be retried this number of times before giving up. (If simply
31
+ # 'true', Sidekiq retries 25 times)
32
+ #
33
+ # We'll add a bit more data to the job to support retries:
34
+ #
35
+ # * 'queue' - the queue to use
36
+ # * 'retry_count' - number of times we've retried so far.
37
+ # * 'error_message' - the message from the exception
38
+ # * 'error_class' - the exception class
39
+ # * 'failed_at' - the first time it failed
40
+ # * 'retried_at' - the last time it was retried
41
+ # * 'backtrace' - the number of lines of error backtrace to store
42
+ #
43
+ # We don't store the backtrace by default as that can add a lot of overhead
44
+ # to the job and everyone is using an error service, right?
45
+ #
46
+ # The default number of retries is 25 which works out to about 3 weeks
47
+ # You can change the default maximum number of retries in your initializer:
48
+ #
49
+ # Sidekiq.options[:max_retries] = 7
50
+ #
51
+ # or limit the number of retries for a particular worker with:
52
+ #
53
+ # class MyWorker
54
+ # include Sidekiq::Worker
55
+ # sidekiq_options :retry => 10
56
+ # end
57
+ #
58
+ class JobRetry
59
+ class Handled < ::RuntimeError; end
60
+ class Skip < Handled; end
61
+
62
+ include Sidekiq::Util
63
+
64
+ DEFAULT_MAX_RETRY_ATTEMPTS = 25
65
+
66
+ def initialize(options = {})
67
+ @max_retries = Sidekiq.options.merge(options).fetch(:max_retries, DEFAULT_MAX_RETRY_ATTEMPTS)
68
+ end
69
+
70
+ # The global retry handler requires only the barest of data.
71
+ # We want to be able to retry as much as possible so we don't
72
+ # require the worker to be instantiated.
73
+ def global(msg, queue)
74
+ yield
75
+ rescue Handled => ex
76
+ raise ex
77
+ rescue Sidekiq::Shutdown => ey
78
+ # ignore, will be pushed back onto queue during hard_shutdown
79
+ raise ey
80
+ rescue Exception => e
81
+ # ignore, will be pushed back onto queue during hard_shutdown
82
+ raise Sidekiq::Shutdown if exception_caused_by_shutdown?(e)
83
+
84
+ if msg['retry']
85
+ attempt_retry(nil, msg, queue, e)
86
+ else
87
+ Sidekiq.death_handlers.each do |handler|
88
+ begin
89
+ handler.call(msg, e)
90
+ rescue => handler_ex
91
+ handle_exception(handler_ex, { context: "Error calling death handler", job: msg })
92
+ end
93
+ end
94
+ end
95
+
96
+ raise Handled
97
+ end
98
+
99
+
100
+ # The local retry support means that any errors that occur within
101
+ # this block can be associated with the given worker instance.
102
+ # This is required to support the `sidekiq_retries_exhausted` block.
103
+ #
104
+ # Note that any exception from the block is wrapped in the Skip
105
+ # exception so the global block does not reprocess the error. The
106
+ # Skip exception is unwrapped within Sidekiq::Processor#process before
107
+ # calling the handle_exception handlers.
108
+ def local(worker, msg, queue)
109
+ yield
110
+ rescue Handled => ex
111
+ raise ex
112
+ rescue Sidekiq::Shutdown => ey
113
+ # ignore, will be pushed back onto queue during hard_shutdown
114
+ raise ey
115
+ rescue Exception => e
116
+ # ignore, will be pushed back onto queue during hard_shutdown
117
+ raise Sidekiq::Shutdown if exception_caused_by_shutdown?(e)
118
+
119
+ if msg['retry'] == nil
120
+ msg['retry'] = worker.class.get_sidekiq_options['retry']
121
+ end
122
+
123
+ raise e unless msg['retry']
124
+ attempt_retry(worker, msg, queue, e)
125
+ # We've handled this error associated with this job, don't
126
+ # need to handle it at the global level
127
+ raise Skip
128
+ end
129
+
130
+ private
131
+
132
+ # Note that +worker+ can be nil here if an error is raised before we can
133
+ # instantiate the worker instance. All access must be guarded and
134
+ # best effort.
135
+ def attempt_retry(worker, msg, queue, exception)
136
+ max_retry_attempts = retry_attempts_from(msg['retry'], @max_retries)
137
+
138
+ msg['queue'] = if msg['retry_queue']
139
+ msg['retry_queue']
140
+ else
141
+ queue
142
+ end
143
+
144
+ m = exception_message(exception)
145
+ if m.respond_to?(:scrub!)
146
+ m.force_encoding("utf-8")
147
+ m.scrub!
148
+ end
149
+
150
+ msg['error_message'] = m
151
+ msg['error_class'] = exception.class.name
152
+ count = if msg['retry_count']
153
+ msg['retried_at'] = Time.now.to_f
154
+ msg['retry_count'] += 1
155
+ else
156
+ msg['failed_at'] = Time.now.to_f
157
+ msg['retry_count'] = 0
158
+ end
159
+
160
+ if msg['backtrace'] == true
161
+ msg['error_backtrace'] = exception.backtrace
162
+ elsif !msg['backtrace']
163
+ # do nothing
164
+ elsif msg['backtrace'].to_i != 0
165
+ msg['error_backtrace'] = exception.backtrace[0...msg['backtrace'].to_i]
166
+ end
167
+
168
+ if count < max_retry_attempts
169
+ delay = delay_for(worker, count, exception)
170
+ # Logging here can break retries if the logging device raises ENOSPC #3979
171
+ #logger.debug { "Failure! Retry #{count} in #{delay} seconds" }
172
+ retry_at = Time.now.to_f + delay
173
+ payload = Sidekiq.dump_json(msg)
174
+ Sidekiq.redis do |conn|
175
+ conn.zadd('retry', retry_at.to_s, payload)
176
+ end
177
+ else
178
+ # Goodbye dear message, you (re)tried your best I'm sure.
179
+ retries_exhausted(worker, msg, exception)
180
+ end
181
+ end
182
+
183
+ def retries_exhausted(worker, msg, exception)
184
+ begin
185
+ block = worker && worker.sidekiq_retries_exhausted_block
186
+ block.call(msg, exception) if block
187
+ rescue => e
188
+ handle_exception(e, { context: "Error calling retries_exhausted", job: msg })
189
+ end
190
+
191
+ Sidekiq.death_handlers.each do |handler|
192
+ begin
193
+ handler.call(msg, exception)
194
+ rescue => e
195
+ handle_exception(e, { context: "Error calling death handler", job: msg })
196
+ end
197
+ end
198
+
199
+ send_to_morgue(msg) unless msg['dead'] == false
200
+ end
201
+
202
+ def send_to_morgue(msg)
203
+ logger.info { "Adding dead #{msg['class']} job #{msg['jid']}" }
204
+ payload = Sidekiq.dump_json(msg)
205
+ DeadSet.new.kill(payload, notify_failure: false)
206
+ end
207
+
208
+ def retry_attempts_from(msg_retry, default)
209
+ if msg_retry.is_a?(Integer)
210
+ msg_retry
211
+ else
212
+ default
213
+ end
214
+ end
215
+
216
+ def delay_for(worker, count, exception)
217
+ if worker && worker.sidekiq_retry_in_block
218
+ custom_retry_in = retry_in(worker, count, exception).to_i
219
+ return custom_retry_in if custom_retry_in > 0
220
+ end
221
+ seconds_to_delay(count)
222
+ end
223
+
224
+ # delayed_job uses the same basic formula
225
+ def seconds_to_delay(count)
226
+ (count ** 4) + 15 + (rand(30)*(count+1))
227
+ end
228
+
229
+ def retry_in(worker, count, exception)
230
+ begin
231
+ worker.sidekiq_retry_in_block.call(count, exception)
232
+ rescue Exception => e
233
+ handle_exception(e, { context: "Failure scheduling retry using the defined `sidekiq_retry_in` in #{worker.class.name}, falling back to default" })
234
+ nil
235
+ end
236
+ end
237
+
238
+ def exception_caused_by_shutdown?(e, checked_causes = [])
239
+ return false unless e.cause
240
+
241
+ # Handle circular causes
242
+ checked_causes << e.object_id
243
+ return false if checked_causes.include?(e.cause.object_id)
244
+
245
+ e.cause.instance_of?(Sidekiq::Shutdown) ||
246
+ exception_caused_by_shutdown?(e.cause, checked_causes)
247
+ end
248
+
249
+ # Extract message from exception.
250
+ # Set a default if the message raises an error
251
+ def exception_message(exception)
252
+ begin
253
+ # App code can stuff all sorts of crazy binary data into the error message
254
+ # that won't convert to JSON.
255
+ exception.message.to_s[0, 10_000]
256
+ rescue
257
+ "!!! ERROR MESSAGE THREW AN ERROR !!!".dup
258
+ end
259
+ end
260
+
261
+ end
262
+ end
@@ -0,0 +1,173 @@
1
+ # frozen_string_literal: true
2
+ require 'sidekiq/manager'
3
+ require 'sidekiq/fetch'
4
+ require 'sidekiq/scheduled'
5
+
6
+ module Sidekiq
7
+ # The Launcher is a very simple Actor whose job is to
8
+ # start, monitor and stop the core Actors in Sidekiq.
9
+ # If any of these actors die, the Sidekiq process exits
10
+ # immediately.
11
+ class Launcher
12
+ include Util
13
+
14
+ attr_accessor :manager, :poller, :fetcher
15
+
16
+ STATS_TTL = 5*365*24*60*60
17
+
18
+ def initialize(options)
19
+ @manager = Sidekiq::Manager.new(options)
20
+ @poller = Sidekiq::Scheduled::Poller.new
21
+ @done = false
22
+ @options = options
23
+ end
24
+
25
+ def run
26
+ @thread = safe_thread("heartbeat", &method(:start_heartbeat))
27
+ @poller.start
28
+ @manager.start
29
+ end
30
+
31
+ # Stops this instance from processing any more jobs,
32
+ #
33
+ def quiet
34
+ @done = true
35
+ @manager.quiet
36
+ @poller.terminate
37
+ end
38
+
39
+ # Shuts down the process. This method does not
40
+ # return until all work is complete and cleaned up.
41
+ # It can take up to the timeout to complete.
42
+ def stop
43
+ deadline = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC) + @options[:timeout]
44
+
45
+ @done = true
46
+ @manager.quiet
47
+ @poller.terminate
48
+
49
+ @manager.stop(deadline)
50
+
51
+ # Requeue everything in case there was a worker who grabbed work while stopped
52
+ # This call is a no-op in Sidekiq but necessary for Sidekiq Pro.
53
+ strategy = (@options[:fetch] || Sidekiq::BasicFetch)
54
+ strategy.bulk_requeue([], @options)
55
+
56
+ clear_heartbeat
57
+ end
58
+
59
+ def stopping?
60
+ @done
61
+ end
62
+
63
+ private unless $TESTING
64
+
65
+ def heartbeat
66
+ results = Sidekiq::CLI::PROCTITLES.map {|x| x.(self, to_data) }
67
+ results.compact!
68
+ $0 = results.join(' ')
69
+
70
+
71
+ end
72
+
73
+ def ❤
74
+ key = identity
75
+ fails = procd = 0
76
+ begin
77
+ fails = Processor::FAILURE.reset
78
+ procd = Processor::PROCESSED.reset
79
+ curstate = Processor::WORKER_STATE.dup
80
+
81
+ workers_key = "#{key}:workers"
82
+ nowdate = Time.now.utc.strftime("%Y-%m-%d")
83
+ Sidekiq.redis do |conn|
84
+ conn.multi do
85
+ conn.incrby("stat:processed", procd)
86
+ conn.incrby("stat:processed:#{nowdate}", procd)
87
+ conn.expire("stat:processed:#{nowdate}", STATS_TTL)
88
+
89
+ conn.incrby("stat:failed", fails)
90
+ conn.incrby("stat:failed:#{nowdate}", fails)
91
+ conn.expire("stat:failed:#{nowdate}", STATS_TTL)
92
+
93
+ conn.del(workers_key)
94
+ curstate.each_pair do |tid, hash|
95
+ conn.hset(workers_key, tid, Sidekiq.dump_json(hash))
96
+ end
97
+ conn.expire(workers_key, 60)
98
+ end
99
+ end
100
+ fails = procd = 0
101
+
102
+ _, exists, _, _, msg = Sidekiq.redis do |conn|
103
+ conn.multi do
104
+ conn.sadd('processes', key)
105
+ conn.exists(key)
106
+ conn.hmset(key, 'info', to_json, 'busy', curstate.size, 'beat', Time.now.to_f, 'quiet', @done)
107
+ conn.expire(key, 60)
108
+ conn.rpop("#{key}-signals")
109
+ end
110
+ end
111
+
112
+ # first heartbeat or recovering from an outage and need to reestablish our heartbeat
113
+ fire_event(:heartbeat) if !exists
114
+
115
+ return unless msg
116
+
117
+ ::Process.kill(msg, $$)
118
+ rescue => e
119
+ # ignore all redis/network issues
120
+ logger.error("heartbeat: #{e.message}")
121
+ # don't lose the counts if there was a network issue
122
+ Processor::PROCESSED.incr(procd)
123
+ Processor::FAILURE.incr(fails)
124
+ end
125
+ end
126
+
127
+ def start_heartbeat
128
+ while true
129
+ heartbeat
130
+ sleep 5
131
+ end
132
+ Sidekiq.logger.info("Heartbeat stopping...")
133
+ end
134
+
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
148
+ end
149
+
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
156
+ end
157
+
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
170
+ end
171
+
172
+ end
173
+ end
@@ -0,0 +1,122 @@
1
+ # frozen_string_literal: true
2
+ require 'time'
3
+ require 'logger'
4
+ require 'fcntl'
5
+
6
+ module Sidekiq
7
+ module Logging
8
+
9
+ class Pretty < Logger::Formatter
10
+ SPACE = " "
11
+
12
+ # Provide a call() method that returns the formatted message.
13
+ def call(severity, time, program_name, message)
14
+ "#{time.utc.iso8601(3)} #{::Process.pid} TID-#{Sidekiq::Logging.tid}#{context} #{severity}: #{message}\n"
15
+ end
16
+
17
+ def context
18
+ c = Thread.current[:sidekiq_context]
19
+ " #{c.join(SPACE)}" if c && c.any?
20
+ end
21
+ end
22
+
23
+ class WithoutTimestamp < Pretty
24
+ def call(severity, time, program_name, message)
25
+ "#{::Process.pid} TID-#{Sidekiq::Logging.tid}#{context} #{severity}: #{message}\n"
26
+ end
27
+ end
28
+
29
+ def self.tid
30
+ Thread.current['sidekiq_tid'] ||= (Thread.current.object_id ^ ::Process.pid).to_s(36)
31
+ end
32
+
33
+ def self.job_hash_context(job_hash)
34
+ # If we're using a wrapper class, like ActiveJob, use the "wrapped"
35
+ # attribute to expose the underlying thing.
36
+ klass = job_hash['wrapped'] || job_hash["class"]
37
+ bid = job_hash['bid']
38
+ "#{klass} JID-#{job_hash['jid']}#{" BID-#{bid}" if bid}"
39
+ end
40
+
41
+ def self.with_job_hash_context(job_hash, &block)
42
+ with_context(job_hash_context(job_hash), &block)
43
+ end
44
+
45
+ def self.with_context(msg)
46
+ Thread.current[:sidekiq_context] ||= []
47
+ Thread.current[:sidekiq_context] << msg
48
+ yield
49
+ ensure
50
+ Thread.current[:sidekiq_context].pop
51
+ end
52
+
53
+ def self.initialize_logger(log_target = STDOUT)
54
+ oldlogger = defined?(@logger) ? @logger : nil
55
+ @logger = Logger.new(log_target)
56
+ @logger.level = Logger::INFO
57
+ @logger.formatter = ENV['DYNO'] ? WithoutTimestamp.new : Pretty.new
58
+ oldlogger.close if oldlogger && !$TESTING # don't want to close testing's STDOUT logging
59
+ @logger
60
+ end
61
+
62
+ def self.logger
63
+ defined?(@logger) ? @logger : initialize_logger
64
+ end
65
+
66
+ def self.logger=(log)
67
+ @logger = (log ? log : Logger.new(File::NULL))
68
+ end
69
+
70
+ # This reopens ALL logfiles in the process that have been rotated
71
+ # using logrotate(8) (without copytruncate) or similar tools.
72
+ # A +File+ object is considered for reopening if it is:
73
+ # 1) opened with the O_APPEND and O_WRONLY flags
74
+ # 2) the current open file handle does not match its original open path
75
+ # 3) unbuffered (as far as userspace buffering goes, not O_SYNC)
76
+ # Returns the number of files reopened
77
+ def self.reopen_logs
78
+ to_reopen = []
79
+ append_flags = File::WRONLY | File::APPEND
80
+
81
+ ObjectSpace.each_object(File) do |fp|
82
+ begin
83
+ if !fp.closed? && fp.stat.file? && fp.sync && (fp.fcntl(Fcntl::F_GETFL) & append_flags) == append_flags
84
+ to_reopen << fp
85
+ end
86
+ rescue IOError, Errno::EBADF
87
+ end
88
+ end
89
+
90
+ nr = 0
91
+ to_reopen.each do |fp|
92
+ orig_st = begin
93
+ fp.stat
94
+ rescue IOError, Errno::EBADF
95
+ next
96
+ end
97
+
98
+ begin
99
+ b = File.stat(fp.path)
100
+ next if orig_st.ino == b.ino && orig_st.dev == b.dev
101
+ rescue Errno::ENOENT
102
+ end
103
+
104
+ begin
105
+ File.open(fp.path, 'a') { |tmpfp| fp.reopen(tmpfp) }
106
+ fp.sync = true
107
+ nr += 1
108
+ rescue IOError, Errno::EBADF
109
+ # not much we can do...
110
+ end
111
+ end
112
+ nr
113
+ rescue RuntimeError => ex
114
+ # RuntimeError: ObjectSpace is disabled; each_object will only work with Class, pass -X+O to enable
115
+ puts "Unable to reopen logs: #{ex.message}"
116
+ end
117
+
118
+ def logger
119
+ Sidekiq::Logging.logger
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,137 @@
1
+ # frozen_string_literal: true
2
+ require 'sidekiq/util'
3
+ require 'sidekiq/processor'
4
+ require 'sidekiq/fetch'
5
+ require 'thread'
6
+ require 'set'
7
+
8
+ module Sidekiq
9
+
10
+ ##
11
+ # The Manager is the central coordination point in Sidekiq, controlling
12
+ # the lifecycle of the Processors.
13
+ #
14
+ # Tasks:
15
+ #
16
+ # 1. start: Spin up Processors.
17
+ # 3. processor_died: Handle job failure, throw away Processor, create new one.
18
+ # 4. quiet: shutdown idle Processors.
19
+ # 5. stop: hard stop the Processors by deadline.
20
+ #
21
+ # Note that only the last task requires its own Thread since it has to monitor
22
+ # the shutdown process. The other tasks are performed by other threads.
23
+ #
24
+ class Manager
25
+ include Util
26
+
27
+ attr_reader :workers
28
+ attr_reader :options
29
+
30
+ def initialize(options={})
31
+ logger.debug { options.inspect }
32
+ @options = options
33
+ @count = options[:concurrency] || 10
34
+ raise ArgumentError, "Concurrency of #{@count} is not supported" if @count < 1
35
+
36
+ @done = false
37
+ @workers = Set.new
38
+ @count.times do
39
+ @workers << Processor.new(self)
40
+ end
41
+ @plock = Mutex.new
42
+ end
43
+
44
+ def start
45
+ @workers.each do |x|
46
+ x.start
47
+ end
48
+ end
49
+
50
+ def quiet
51
+ return if @done
52
+ @done = true
53
+
54
+ logger.info { "Terminating quiet workers" }
55
+ @workers.each { |x| x.terminate }
56
+ fire_event(:quiet, reverse: true)
57
+ end
58
+
59
+ # hack for quicker development / testing environment #2774
60
+ PAUSE_TIME = STDOUT.tty? ? 0.1 : 0.5
61
+
62
+ def stop(deadline)
63
+ quiet
64
+ fire_event(:shutdown, reverse: true)
65
+
66
+ # some of the shutdown events can be async,
67
+ # we don't have any way to know when they're done but
68
+ # give them a little time to take effect
69
+ sleep PAUSE_TIME
70
+ return if @workers.empty?
71
+
72
+ logger.info { "Pausing to allow workers to finish..." }
73
+ remaining = deadline - ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
74
+ while remaining > PAUSE_TIME
75
+ return if @workers.empty?
76
+ sleep PAUSE_TIME
77
+ remaining = deadline - ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
78
+ end
79
+ return if @workers.empty?
80
+
81
+ hard_shutdown
82
+ end
83
+
84
+ def processor_stopped(processor)
85
+ @plock.synchronize do
86
+ @workers.delete(processor)
87
+ end
88
+ end
89
+
90
+ def processor_died(processor, reason)
91
+ @plock.synchronize do
92
+ @workers.delete(processor)
93
+ unless @done
94
+ p = Processor.new(self)
95
+ @workers << p
96
+ p.start
97
+ end
98
+ end
99
+ end
100
+
101
+ def stopped?
102
+ @done
103
+ end
104
+
105
+ private
106
+
107
+ def hard_shutdown
108
+ # We've reached the timeout and we still have busy workers.
109
+ # They must die but their jobs shall live on.
110
+ cleanup = nil
111
+ @plock.synchronize do
112
+ cleanup = @workers.dup
113
+ end
114
+
115
+ if cleanup.size > 0
116
+ jobs = cleanup.map {|p| p.job }.compact
117
+
118
+ logger.warn { "Terminating #{cleanup.size} busy worker threads" }
119
+ logger.warn { "Work still in progress #{jobs.inspect}" }
120
+
121
+ # Re-enqueue unfinished jobs
122
+ # NOTE: You may notice that we may push a job back to redis before
123
+ # the worker thread is terminated. This is ok because Sidekiq's
124
+ # contract says that jobs are run AT LEAST once. Process termination
125
+ # is delayed until we're certain the jobs are back in Redis because
126
+ # it is worse to lose a job than to run it twice.
127
+ strategy = (@options[:fetch] || Sidekiq::BasicFetch)
128
+ strategy.bulk_requeue(jobs, @options)
129
+ end
130
+
131
+ cleanup.each do |processor|
132
+ processor.kill
133
+ end
134
+ end
135
+
136
+ end
137
+ end