resque 1.27.4 → 2.6.0

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.
Files changed (45) hide show
  1. checksums.yaml +5 -5
  2. data/HISTORY.md +122 -3
  3. data/README.markdown +441 -500
  4. data/bin/resque-web +10 -26
  5. data/lib/resque/data_store.rb +52 -58
  6. data/lib/resque/errors.rb +7 -1
  7. data/lib/resque/failure/airbrake.rb +19 -7
  8. data/lib/resque/failure/multiple.rb +6 -2
  9. data/lib/resque/failure/redis.rb +1 -1
  10. data/lib/resque/failure/redis_multi_queue.rb +1 -1
  11. data/lib/resque/failure.rb +7 -0
  12. data/lib/resque/job.rb +2 -2
  13. data/lib/resque/logging.rb +1 -1
  14. data/lib/resque/railtie.rb +10 -0
  15. data/lib/resque/server/public/jquery-3.6.0.min.js +2 -0
  16. data/lib/resque/server/public/main.js +3 -0
  17. data/lib/resque/server/public/ranger.js +7 -4
  18. data/lib/resque/server/public/style.css +3 -3
  19. data/lib/resque/server/views/error.erb +1 -1
  20. data/lib/resque/server/views/failed.erb +9 -3
  21. data/lib/resque/server/views/failed_job.erb +2 -2
  22. data/lib/resque/server/views/job_class.erb +3 -1
  23. data/lib/resque/server/views/key_string.erb +1 -1
  24. data/lib/resque/server/views/layout.erb +5 -4
  25. data/lib/resque/server/views/next_more.erb +14 -14
  26. data/lib/resque/server/views/queues.erb +6 -6
  27. data/lib/resque/server/views/stats.erb +5 -5
  28. data/lib/resque/server/views/working.erb +7 -7
  29. data/lib/resque/server.rb +11 -119
  30. data/lib/resque/server_helper.rb +185 -0
  31. data/lib/resque/stat.rb +16 -9
  32. data/lib/resque/tasks.rb +3 -11
  33. data/lib/resque/thread_signal.rb +13 -34
  34. data/lib/resque/vendor/utf8_util.rb +2 -8
  35. data/lib/resque/version.rb +1 -1
  36. data/lib/resque/web_runner.rb +374 -0
  37. data/lib/resque/worker.rb +76 -48
  38. data/lib/resque.rb +100 -27
  39. data/lib/tasks/redis.rake +10 -10
  40. metadata +44 -28
  41. data/lib/resque/server/helpers.rb +0 -64
  42. data/lib/resque/server/public/jquery-1.12.4.min.js +0 -5
  43. data/lib/resque/server/test_helper.rb +0 -19
  44. data/lib/resque/vendor/utf8_util/utf8_util_18.rb +0 -91
  45. data/lib/resque/vendor/utf8_util/utf8_util_19.rb +0 -6
@@ -0,0 +1,374 @@
1
+ require 'open-uri'
2
+ require 'logger'
3
+ require 'optparse'
4
+ require 'fileutils'
5
+ require 'rack'
6
+ require 'resque/server'
7
+
8
+ # only used with `bin/resque-web`
9
+ # https://github.com/resque/resque/pull/1780
10
+
11
+ module Resque
12
+ WINDOWS = !!(RUBY_PLATFORM =~ /(mingw|bccwin|wince|mswin32)/i)
13
+ JRUBY = !!(RbConfig::CONFIG["RUBY_INSTALL_NAME"] =~ /^jruby/i)
14
+
15
+ class WebRunner
16
+ attr_reader :app, :app_name, :filesystem_friendly_app_name,
17
+ :rack_handler, :port, :options, :args
18
+
19
+ PORT = 5678
20
+ HOST = WINDOWS ? 'localhost' : '0.0.0.0'
21
+
22
+ def initialize(*runtime_args)
23
+ @options = runtime_args.last.is_a?(Hash) ? runtime_args.pop : {}
24
+
25
+ self.class.logger.level = options[:debug] ? Logger::DEBUG : Logger::INFO
26
+
27
+ @app = Resque::Server
28
+ @app_name = 'resque-web'
29
+ @filesystem_friendly_app_name = @app_name.gsub(/\W+/, "_")
30
+
31
+ @args = load_options(runtime_args)
32
+
33
+ @rack_handler = (s = options[:rack_handler]) ? Rack::Handler.get(s) : setup_rack_handler
34
+
35
+ case option_parser.command
36
+ when :help
37
+ puts option_parser
38
+ when :kill
39
+ kill!
40
+ when :status
41
+ status
42
+ when :version
43
+ puts "resque #{Resque::VERSION}"
44
+ puts "rack #{Rack::VERSION.join('.')}"
45
+ puts "sinatra #{Sinatra::VERSION}" if defined?(Sinatra)
46
+ else
47
+ before_run
48
+ start unless options[:start] == false
49
+ end
50
+ end
51
+
52
+ def launch_path
53
+ if options[:launch_path].respond_to?(:call)
54
+ options[:launch_path].call(self)
55
+ else
56
+ options[:launch_path]
57
+ end
58
+ end
59
+
60
+ def app_dir
61
+ if !options[:app_dir] && !ENV['HOME']
62
+ raise ArgumentError.new("nor --app-dir neither ENV['HOME'] defined")
63
+ end
64
+ options[:app_dir] || File.join(ENV['HOME'], filesystem_friendly_app_name)
65
+ end
66
+
67
+ def pid_file
68
+ options[:pid_file] || File.join(app_dir, "#{filesystem_friendly_app_name}.pid")
69
+ end
70
+
71
+ def url_file
72
+ options[:url_file] || File.join(app_dir, "#{filesystem_friendly_app_name}.url")
73
+ end
74
+
75
+ def log_file
76
+ options[:log_file] || File.join(app_dir, "#{filesystem_friendly_app_name}.log")
77
+ end
78
+
79
+ def host
80
+ options.fetch(:host) { HOST }
81
+ end
82
+
83
+ def url
84
+ "http://#{host}:#{port}"
85
+ end
86
+
87
+ def before_run
88
+ if (redis_conf = options[:redis_conf])
89
+ logger.info "Using Redis connection '#{redis_conf}'"
90
+ Resque.redis = redis_conf
91
+ end
92
+ if (namespace = options[:redis_namespace])
93
+ logger.info "Using Redis namespace '#{namespace}'"
94
+ Resque.redis.namespace = namespace
95
+ end
96
+ if (url_prefix = options[:url_prefix])
97
+ logger.info "Using URL Prefix '#{url_prefix}'"
98
+ Resque::Server.url_prefix = url_prefix
99
+ end
100
+ app.set(options.merge web_runner: self)
101
+ path = (ENV['RESQUECONFIG'] || args.first)
102
+ load_config_file(path.to_s.strip) if path
103
+ end
104
+
105
+ def start(path = launch_path)
106
+ logger.info "Running with Windows Settings" if WINDOWS
107
+ logger.info "Running with JRuby" if JRUBY
108
+ logger.info "Starting '#{app_name}'..."
109
+
110
+ check_for_running(path)
111
+ find_port
112
+ write_url
113
+ launch!(url, path)
114
+ daemonize! unless options[:foreground]
115
+ run!
116
+ rescue RuntimeError => e
117
+ logger.warn "There was an error starting '#{app_name}': #{e}"
118
+ exit
119
+ end
120
+
121
+ def find_port
122
+ if @port = options[:port]
123
+ announce_port_attempted
124
+
125
+ unless port_open?
126
+ logger.warn "Port #{port} is already in use. Please try another. " +
127
+ "You can also omit the port flag, and we'll find one for you."
128
+ end
129
+ else
130
+ @port = PORT
131
+ announce_port_attempted
132
+
133
+ until port_open?
134
+ @port += 1
135
+ announce_port_attempted
136
+ end
137
+ end
138
+ end
139
+
140
+ def announce_port_attempted
141
+ logger.info "trying port #{port}..."
142
+ end
143
+
144
+ def port_open?(check_url = nil)
145
+ begin
146
+ check_url ||= url
147
+ options[:no_proxy] ? uri_open(check_url, :proxy => nil) : uri_open(check_url)
148
+ false
149
+ rescue Errno::ECONNREFUSED, Errno::EPERM, Errno::ETIMEDOUT
150
+ true
151
+ end
152
+ end
153
+
154
+ def uri_open(*args)
155
+ (RbConfig::CONFIG['ruby_version'] < '2.7') ? open(*args) : URI.open(*args)
156
+ end
157
+
158
+ def write_url
159
+ # Make sure app dir is setup
160
+ FileUtils.mkdir_p(app_dir)
161
+ File.open(url_file, 'w') {|f| f << url }
162
+ end
163
+
164
+ def check_for_running(path = nil)
165
+ if File.exist?(pid_file) && File.exist?(url_file)
166
+ running_url = File.read(url_file)
167
+ if !port_open?(running_url)
168
+ logger.warn "'#{app_name}' is already running at #{running_url}"
169
+ launch!(running_url, path)
170
+ exit!(1)
171
+ end
172
+ end
173
+ end
174
+
175
+ def run!
176
+ logger.info "Running with Rack handler: #{@rack_handler.inspect}"
177
+
178
+ rack_handler.run app, :Host => host, :Port => port do |server|
179
+ kill_commands.each do |command|
180
+ trap(command) do
181
+ ## Use thins' hard #stop! if available, otherwise just #stop
182
+ server.respond_to?(:stop!) ? server.stop! : server.stop
183
+ logger.info "'#{app_name}' received INT ... stopping"
184
+ delete_pid!
185
+ end
186
+ end
187
+ end
188
+ end
189
+
190
+ # Adapted from Rackup
191
+ def daemonize!
192
+ if JRUBY
193
+ # It's not a true daemon but when executed with & works like one
194
+ thread = Thread.new {daemon_execute}
195
+ thread.join
196
+
197
+ elsif RUBY_VERSION < "1.9"
198
+ logger.debug "Parent Process: #{Process.pid}"
199
+ exit!(0) if fork
200
+ logger.debug "Child Process: #{Process.pid}"
201
+ daemon_execute
202
+
203
+ else
204
+ Process.daemon(true, true)
205
+ daemon_execute
206
+ end
207
+ end
208
+
209
+ def daemon_execute
210
+ File.umask 0000
211
+ FileUtils.touch log_file
212
+ STDIN.reopen log_file
213
+ STDOUT.reopen log_file, "a"
214
+ STDERR.reopen log_file, "a"
215
+
216
+ logger.debug "Child Process: #{Process.pid}"
217
+
218
+ File.open(pid_file, 'w') {|f| f.write("#{Process.pid}") }
219
+ at_exit { delete_pid! }
220
+ end
221
+
222
+ def launch!(specific_url = nil, path = nil)
223
+ return if options[:skip_launch]
224
+ cmd = WINDOWS ? "start" : "open"
225
+ system "#{cmd} #{specific_url || url}#{path}"
226
+ end
227
+
228
+ def kill!
229
+ pid = File.read(pid_file)
230
+ logger.warn "Sending #{kill_command} to #{pid.to_i}"
231
+ Process.kill(kill_command, pid.to_i)
232
+ rescue => e
233
+ logger.warn "pid not found at #{pid_file} : #{e}"
234
+ end
235
+
236
+ def status
237
+ if File.exist?(pid_file)
238
+ logger.info "'#{app_name}' running"
239
+ logger.info "PID #{File.read(pid_file)}"
240
+ logger.info "URL #{File.read(url_file)}" if File.exist?(url_file)
241
+ else
242
+ logger.info "'#{app_name}' not running!"
243
+ end
244
+ end
245
+
246
+ # Loads a config file at config_path and evals it in the context of the @app.
247
+ def load_config_file(config_path)
248
+ abort "Can not find config file at #{config_path}" if !File.readable?(config_path)
249
+ config = File.read(config_path)
250
+ # trim off anything after __END__
251
+ config.sub!(/^__END__\n.*/, '')
252
+ @app.module_eval(config)
253
+ end
254
+
255
+ def self.logger=(logger)
256
+ @logger = logger
257
+ end
258
+
259
+ def self.logger
260
+ @logger ||= LOGGER if defined?(LOGGER)
261
+ if !@logger
262
+ @logger = Logger.new(STDOUT)
263
+ @logger.formatter = Proc.new {|s, t, n, msg| "[#{t}] #{msg}\n"}
264
+ @logger
265
+ end
266
+ @logger
267
+ end
268
+
269
+ def logger
270
+ self.class.logger
271
+ end
272
+
273
+ private
274
+ def setup_rack_handler
275
+ # First try to set Rack handler via a special hook we honor
276
+ @rack_handler = if @app.respond_to?(:detect_rack_handler)
277
+ @app.detect_rack_handler
278
+
279
+ # If they aren't using our hook, try to use their @app.server settings
280
+ elsif @app.respond_to?(:server) and @app.server
281
+ # If :server isn't set, it returns an array of possibilities,
282
+ # sorted from most to least preferable.
283
+ if @app.server.is_a?(Array)
284
+ handler = nil
285
+ @app.server.each do |server|
286
+ begin
287
+ handler = Rack::Handler.get(server)
288
+ break
289
+ rescue LoadError, NameError
290
+ next
291
+ end
292
+ end
293
+ raise 'No available Rack handler (e.g. WEBrick, Thin, Puma, etc.) was found.' if handler.nil?
294
+
295
+ handler
296
+
297
+ # :server might be set explicitly to a single option like "mongrel"
298
+ else
299
+ Rack::Handler.get(@app.server)
300
+ end
301
+
302
+ # If all else fails, we'll use Thin
303
+ else
304
+ JRUBY ? Rack::Handler::WEBrick : Rack::Handler::Thin
305
+ end
306
+ end
307
+
308
+ def load_options(runtime_args)
309
+ @args = option_parser.parse!(runtime_args)
310
+ options.merge!(option_parser.options)
311
+ args
312
+ rescue OptionParser::MissingArgument => e
313
+ logger.warn "#{e}, run -h for options"
314
+ exit
315
+ end
316
+
317
+ def option_parser
318
+ @option_parser ||= Parser.new(app_name)
319
+ end
320
+
321
+ class Parser < OptionParser
322
+ attr_reader :command, :options
323
+
324
+ def initialize(app_name)
325
+ super("", 24, ' ')
326
+ self.banner = "Usage: #{app_name} [options]"
327
+
328
+ @options = {}
329
+ basename = app_name.gsub(/\W+/, "_")
330
+ on('-K', "--kill", "kill the running process and exit") { @command = :kill }
331
+ on('-S', "--status", "display the current running PID and URL then quit") { @command = :status }
332
+ string_option("-s", "--server SERVER", "serve using SERVER (thin/mongrel/webrick)", :rack_handler)
333
+ string_option("-o", "--host HOST", "listen on HOST (default: #{HOST})", :host)
334
+ string_option("-p", "--port PORT", "use PORT (default: #{PORT})", :port)
335
+ on("-x", "--no-proxy", "ignore env proxy settings (e.g. http_proxy)") { opts[:no_proxy] = true }
336
+ boolean_option("-F", "--foreground", "don't daemonize, run in the foreground", :foreground)
337
+ boolean_option("-L", "--no-launch", "don't launch the browser", :skip_launch)
338
+ boolean_option('-d', "--debug", "raise the log level to :debug (default: :info)", :debug)
339
+ string_option("--app-dir APP_DIR", "set the app dir where files are stored (default: ~/#{basename}/)", :app_dir)
340
+ string_option("-P", "--pid-file PID_FILE", "set the path to the pid file (default: app_dir/#{basename}.pid)", :pid_file)
341
+ string_option("--log-file LOG_FILE", "set the path to the log file (default: app_dir/#{basename}.log)", :log_file)
342
+ string_option("--url-file URL_FILE", "set the path to the URL file (default: app_dir/#{basename}.url)", :url_file)
343
+ string_option('-N NAMESPACE', "--namespace NAMESPACE", "set the Redis namespace", :redis_namespace)
344
+ string_option('-r redis-connection', "--redis redis-connection", "set the Redis connection string", :redis_conf)
345
+ string_option('-a url-prefix', "--append url-prefix", "set reverse_proxy friendly prefix to links", :url_prefix)
346
+ separator ""
347
+ separator "Common options:"
348
+ on_tail("-h", "--help", "Show this message") { @command = :help }
349
+ on_tail("--version", "Show version") { @command = :version }
350
+ end
351
+
352
+ def boolean_option(*argv)
353
+ k = argv.pop; on(*argv) { options[k] = true }
354
+ end
355
+
356
+ def string_option(*argv)
357
+ k = argv.pop; on(*argv) { |value| options[k] = value }
358
+ end
359
+ end
360
+
361
+ def kill_commands
362
+ WINDOWS ? [1] : [:INT, :TERM]
363
+ end
364
+
365
+ def kill_command
366
+ kill_commands[0]
367
+ end
368
+
369
+ def delete_pid!
370
+ File.delete(pid_file) if File.exist?(pid_file)
371
+ end
372
+ end
373
+
374
+ end
data/lib/resque/worker.rb CHANGED
@@ -103,7 +103,7 @@ module Resque
103
103
  skip_exists = options[:skip_exists]
104
104
 
105
105
  if skip_exists || exists?(worker_id)
106
- host, pid, queues_raw = worker_id.split(':')
106
+ host, pid, queues_raw = worker_id.split(':', 3)
107
107
  queues = queues_raw.split(',')
108
108
  worker = new(*queues)
109
109
  worker.hostname = host
@@ -145,6 +145,11 @@ module Resque
145
145
  @paused = nil
146
146
  @before_first_fork_hook_ran = false
147
147
 
148
+ @heartbeat_thread = nil
149
+ @heartbeat_thread_signal = nil
150
+
151
+ @last_state = :idle
152
+
148
153
  verbose_value = ENV['LOGGING'] || ENV['VERBOSE']
149
154
  self.verbose = verbose_value if verbose_value
150
155
  self.very_verbose = ENV['VVERBOSE'] if ENV['VVERBOSE']
@@ -162,9 +167,6 @@ module Resque
162
167
  # once per worker.
163
168
  def prepare
164
169
  if ENV['BACKGROUND']
165
- unless Process.respond_to?('daemon')
166
- abort "env var BACKGROUND is set, which requires ruby >= 1.9"
167
- end
168
170
  Process.daemon(true)
169
171
  end
170
172
 
@@ -175,12 +177,20 @@ module Resque
175
177
  self.reconnect if ENV['BACKGROUND']
176
178
  end
177
179
 
180
+ WILDCARDS = ['*', '?', '{', '}', '[', ']'].freeze
181
+
178
182
  def queues=(queues)
179
- queues = queues.empty? ? (ENV["QUEUES"] || ENV['QUEUE']).to_s.split(',') : queues
180
- @queues = queues.map { |queue| queue.to_s.strip }
181
- unless ['*', '?', '{', '}', '[', ']'].any? {|char| @queues.join.include?(char) }
182
- @static_queues = @queues.flatten.uniq
183
- end
183
+ queues = (ENV["QUEUES"] || ENV['QUEUE']).to_s.split(',') if queues.empty?
184
+ queues = queues.map { |queue| queue.to_s.strip }
185
+
186
+ @skip_queues, @queues = queues.partition { |queue| queue.start_with?('!') }
187
+ @skip_queues.map! { |queue| queue[1..-1] }
188
+
189
+ # The behavior of `queues` is dependent on the value of `@has_dynamic_queues: if it's true, the method returns the result of filtering @queues with `glob_match`
190
+ # if it's false, the method returns @queues directly. Since `glob_match` will cause skipped queues to be filtered out, we want to make sure it's called if we have @skip_queues.any?
191
+ @has_dynamic_queues =
192
+ @skip_queues.any? || WILDCARDS.any? { |char| @queues.join.include?(char) }
193
+
184
194
  validate_queues
185
195
  end
186
196
 
@@ -198,13 +208,18 @@ module Resque
198
208
  # A splat ("*") means you want every queue (in alpha order) - this
199
209
  # can be useful for dynamically adding new queues.
200
210
  def queues
201
- return @static_queues if @static_queues
202
- @queues.map { |queue| glob_match(queue) }.flatten.uniq
211
+ if @has_dynamic_queues
212
+ current_queues = Resque.queues
213
+ @queues.map { |queue| glob_match(current_queues, queue) }.flatten.uniq
214
+ else
215
+ @queues
216
+ end
203
217
  end
204
218
 
205
- def glob_match(pattern)
206
- Resque.queues.select do |queue|
207
- File.fnmatch?(pattern, queue)
219
+ def glob_match(list, pattern)
220
+ list.select do |queue|
221
+ File.fnmatch?(pattern, queue) &&
222
+ @skip_queues.none? { |skip_pattern| File.fnmatch?(skip_pattern, queue) }
208
223
  end.sort
209
224
  end
210
225
 
@@ -232,6 +247,7 @@ module Resque
232
247
  break if shutdown?
233
248
 
234
249
  unless work_one_job(&block)
250
+ state_change
235
251
  break if interval.zero?
236
252
  log_with_severity :debug, "Sleeping for #{interval} seconds"
237
253
  procline paused? ? "Paused" : "Waiting for #{queues.join(',')}"
@@ -240,10 +256,12 @@ module Resque
240
256
  end
241
257
 
242
258
  unregister_worker
259
+ run_hook :worker_exit
243
260
  rescue Exception => exception
244
261
  return if exception.class == SystemExit && !@child && run_at_exit_hooks
245
262
  log_with_severity :error, "Failed to start worker : #{exception.inspect}"
246
263
  unregister_worker(exception)
264
+ run_hook :worker_exit
247
265
  end
248
266
 
249
267
  def work_one_job(job = nil, &block)
@@ -482,20 +500,22 @@ module Resque
482
500
  # Returns a list of workers that have sent a heartbeat in the past, but which
483
501
  # already expired (does NOT include workers that have never sent a heartbeat at all).
484
502
  def self.all_workers_with_expired_heartbeats
485
- workers = Worker.all
503
+ # Use `Worker.all_heartbeats` instead of `Worker.all`
504
+ # to prune workers which haven't been registered but have set a heartbeat.
505
+ # https://github.com/resque/resque/pull/1751
486
506
  heartbeats = Worker.all_heartbeats
487
507
  now = data_store.server_time
488
508
 
489
- workers.select do |worker|
490
- id = worker.to_s
491
- heartbeat = heartbeats[id]
492
-
509
+ heartbeats.select do |id, heartbeat|
493
510
  if heartbeat
494
511
  seconds_since_heartbeat = (now - Time.parse(heartbeat)).to_i
495
512
  seconds_since_heartbeat > Resque.prune_interval
496
513
  else
497
514
  false
498
515
  end
516
+ end.each_key.map do |id|
517
+ # skip_exists must be true to include not registered workers
518
+ find(id, :skip_exists => true)
499
519
  end
500
520
  end
501
521
 
@@ -559,7 +579,7 @@ module Resque
559
579
 
560
580
  # are we paused?
561
581
  def paused?
562
- @paused
582
+ @paused || redis.get('pause-all-workers').to_s.strip.downcase == 'true'
563
583
  end
564
584
 
565
585
  # Stop processing jobs after the current one has completed (if we're
@@ -588,23 +608,27 @@ module Resque
588
608
  # By checking the current Redis state against the actual
589
609
  # environment, we can determine if Redis is old and clean it up a bit.
590
610
  def prune_dead_workers
591
- all_workers = Worker.all
611
+ return unless data_store.acquire_pruning_dead_worker_lock(self, Resque.heartbeat_interval)
592
612
 
593
- unless all_workers.empty?
594
- known_workers = worker_pids
595
- all_workers_with_expired_heartbeats = Worker.all_workers_with_expired_heartbeats
596
- end
613
+ all_workers = Worker.all
597
614
 
598
- all_workers.each do |worker|
615
+ known_workers = worker_pids
616
+ all_workers_with_expired_heartbeats = Worker.all_workers_with_expired_heartbeats
617
+ all_workers_with_expired_heartbeats.each do |worker|
599
618
  # If the worker hasn't sent a heartbeat, remove it from the registry.
600
619
  #
601
620
  # If the worker hasn't ever sent a heartbeat, we won't remove it since
602
621
  # the first heartbeat is sent before the worker is registred it means
603
622
  # that this is a worker that doesn't support heartbeats, e.g., another
604
623
  # client library or an older version of Resque. We won't touch these.
624
+ log_with_severity :info, "Pruning dead worker: #{worker}"
625
+
626
+ job_class = worker.job(false)['payload']['class'] rescue nil
627
+ worker.unregister_worker(PruneDeadWorkerDirtyExit.new(worker.to_s, job_class))
628
+ end
629
+
630
+ all_workers.each do |worker|
605
631
  if all_workers_with_expired_heartbeats.include?(worker)
606
- log_with_severity :info, "Pruning dead worker: #{worker}"
607
- worker.unregister_worker(PruneDeadWorkerDirtyExit.new(worker.to_s))
608
632
  next
609
633
  end
610
634
 
@@ -635,7 +659,8 @@ module Resque
635
659
 
636
660
  # Runs a named hook, passing along any arguments.
637
661
  def run_hook(name, *args)
638
- return unless hooks = Resque.send(name)
662
+ hooks = Resque.send(name)
663
+ return if hooks.empty?
639
664
  return if name == :before_first_fork && @before_first_fork_hook_ran
640
665
  msg = "Running #{name} hooks"
641
666
  msg << " with #{args.inspect}" if args.any?
@@ -672,9 +697,9 @@ module Resque
672
697
 
673
698
  kill_background_threads
674
699
 
675
- data_store.unregister_worker(self) do
676
- Stat.clear("processed:#{self}")
677
- Stat.clear("failed:#{self}")
700
+ data_store.unregister_worker(self) do |**opts|
701
+ Stat.clear("processed:#{self}", **opts)
702
+ Stat.clear("failed:#{self}", **opts)
678
703
  end
679
704
  rescue Exception => exception_while_unregistering
680
705
  message = exception_while_unregistering.message
@@ -695,13 +720,22 @@ module Resque
695
720
  :run_at => Time.now.utc.iso8601,
696
721
  :payload => job.payload
697
722
  data_store.set_worker_payload(self,data)
723
+ state_change
698
724
  end
699
725
 
700
726
  # Called when we are done working - clears our `working_on` state
701
727
  # and tells Redis we processed a job.
702
728
  def done_working
703
- data_store.worker_done_working(self) do
704
- processed!
729
+ data_store.worker_done_working(self) do |**opts|
730
+ processed!(**opts)
731
+ end
732
+ end
733
+
734
+ def state_change
735
+ current_state = state
736
+ if current_state != @last_state
737
+ run_hook :queue_empty if current_state == :idle
738
+ @last_state = current_state
705
739
  end
706
740
  end
707
741
 
@@ -711,9 +745,9 @@ module Resque
711
745
  end
712
746
 
713
747
  # Tell Redis we've processed a job.
714
- def processed!
715
- Stat << "processed"
716
- Stat << "processed:#{self}"
748
+ def processed!(**opts)
749
+ Stat.incr("processed", 1, **opts)
750
+ Stat.incr("processed:#{self}", 1, **opts)
717
751
  end
718
752
 
719
753
  # How many failed jobs has this worker seen? Returns an int.
@@ -808,7 +842,7 @@ module Resque
808
842
  # machine. Useful when pruning dead workers on startup.
809
843
  def windows_worker_pids
810
844
  tasklist_output = `tasklist /FI "IMAGENAME eq ruby.exe" /FO list`.encode("UTF-8", Encoding.locale_charmap)
811
- tasklist_output.split($/).select { |line| line =~ /^PID:/}.collect{ |line| line.gsub /PID:\s+/, '' }
845
+ tasklist_output.split($/).select { |line| line =~ /^PID:/ }.collect { |line| line.gsub(/PID:\s+/, '') }
812
846
  end
813
847
 
814
848
  # Find Resque worker pids on Linux and OS X.
@@ -827,7 +861,7 @@ module Resque
827
861
  `ps -A -o pid,comm | grep "[r]uby" | grep -v "resque-web"`.split("\n").map do |line|
828
862
  real_pid = line.split(' ')[0]
829
863
  pargs_command = `pargs -a #{real_pid} 2>/dev/null | grep [r]esque | grep -v "resque-web"`
830
- if pargs_command.split(':')[1] == " resque-#{Resque::Version}"
864
+ if pargs_command.split(':')[1] == " resque-#{Resque::VERSION}"
831
865
  real_pid
832
866
  end
833
867
  end.compact
@@ -837,7 +871,7 @@ module Resque
837
871
  # Procline is always in the format of:
838
872
  # RESQUE_PROCLINE_PREFIXresque-VERSION: STRING
839
873
  def procline(string)
840
- $0 = "#{ENV['RESQUE_PROCLINE_PREFIX']}resque-#{Resque::Version}: #{string}"
874
+ $0 = "#{ENV['RESQUE_PROCLINE_PREFIX']}resque-#{Resque::VERSION}: #{string}"
841
875
  log_with_severity :debug, $0
842
876
  end
843
877
 
@@ -850,13 +884,7 @@ module Resque
850
884
  end
851
885
 
852
886
 
853
- def verbose
854
- @verbose
855
- end
856
-
857
- def very_verbose
858
- @very_verbose
859
- end
887
+ attr_reader :verbose, :very_verbose
860
888
 
861
889
  def verbose=(value);
862
890
  if value && !very_verbose
@@ -909,7 +937,7 @@ module Resque
909
937
  nil
910
938
  end
911
939
 
912
- job.fail(DirtyExit.new("Child process received unhandled signal #{$?.stopsig}", $?)) if $?.signaled?
940
+ job.fail(DirtyExit.new("Child process received unhandled signal #{$?}", $?)) if $?.signaled?
913
941
  @child = nil
914
942
  end
915
943