resque 2.0.0 → 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.
@@ -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
@@ -148,6 +148,8 @@ module Resque
148
148
  @heartbeat_thread = nil
149
149
  @heartbeat_thread_signal = nil
150
150
 
151
+ @last_state = :idle
152
+
151
153
  verbose_value = ENV['LOGGING'] || ENV['VERBOSE']
152
154
  self.verbose = verbose_value if verbose_value
153
155
  self.very_verbose = ENV['VVERBOSE'] if ENV['VVERBOSE']
@@ -178,9 +180,17 @@ module Resque
178
180
  WILDCARDS = ['*', '?', '{', '}', '[', ']'].freeze
179
181
 
180
182
  def queues=(queues)
181
- queues = queues.empty? ? (ENV["QUEUES"] || ENV['QUEUE']).to_s.split(',') : queues
182
- @queues = queues.map { |queue| queue.to_s.strip }
183
- @has_dynamic_queues = WILDCARDS.any? {|char| @queues.join.include?(char) }
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
 
@@ -208,7 +218,8 @@ module Resque
208
218
 
209
219
  def glob_match(list, pattern)
210
220
  list.select do |queue|
211
- File.fnmatch?(pattern, queue)
221
+ File.fnmatch?(pattern, queue) &&
222
+ @skip_queues.none? { |skip_pattern| File.fnmatch?(skip_pattern, queue) }
212
223
  end.sort
213
224
  end
214
225
 
@@ -236,6 +247,7 @@ module Resque
236
247
  break if shutdown?
237
248
 
238
249
  unless work_one_job(&block)
250
+ state_change
239
251
  break if interval.zero?
240
252
  log_with_severity :debug, "Sleeping for #{interval} seconds"
241
253
  procline paused? ? "Paused" : "Waiting for #{queues.join(',')}"
@@ -244,10 +256,12 @@ module Resque
244
256
  end
245
257
 
246
258
  unregister_worker
259
+ run_hook :worker_exit
247
260
  rescue Exception => exception
248
261
  return if exception.class == SystemExit && !@child && run_at_exit_hooks
249
262
  log_with_severity :error, "Failed to start worker : #{exception.inspect}"
250
263
  unregister_worker(exception)
264
+ run_hook :worker_exit
251
265
  end
252
266
 
253
267
  def work_one_job(job = nil, &block)
@@ -486,20 +500,22 @@ module Resque
486
500
  # Returns a list of workers that have sent a heartbeat in the past, but which
487
501
  # already expired (does NOT include workers that have never sent a heartbeat at all).
488
502
  def self.all_workers_with_expired_heartbeats
489
- 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
490
506
  heartbeats = Worker.all_heartbeats
491
507
  now = data_store.server_time
492
508
 
493
- workers.select do |worker|
494
- id = worker.to_s
495
- heartbeat = heartbeats[id]
496
-
509
+ heartbeats.select do |id, heartbeat|
497
510
  if heartbeat
498
511
  seconds_since_heartbeat = (now - Time.parse(heartbeat)).to_i
499
512
  seconds_since_heartbeat > Resque.prune_interval
500
513
  else
501
514
  false
502
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)
503
519
  end
504
520
  end
505
521
 
@@ -563,7 +579,7 @@ module Resque
563
579
 
564
580
  # are we paused?
565
581
  def paused?
566
- @paused
582
+ @paused || redis.get('pause-all-workers').to_s.strip.downcase == 'true'
567
583
  end
568
584
 
569
585
  # Stop processing jobs after the current one has completed (if we're
@@ -596,23 +612,23 @@ module Resque
596
612
 
597
613
  all_workers = Worker.all
598
614
 
599
- unless all_workers.empty?
600
- known_workers = worker_pids
601
- all_workers_with_expired_heartbeats = Worker.all_workers_with_expired_heartbeats
602
- end
603
-
604
- 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|
605
618
  # If the worker hasn't sent a heartbeat, remove it from the registry.
606
619
  #
607
620
  # If the worker hasn't ever sent a heartbeat, we won't remove it since
608
621
  # the first heartbeat is sent before the worker is registred it means
609
622
  # that this is a worker that doesn't support heartbeats, e.g., another
610
623
  # client library or an older version of Resque. We won't touch these.
611
- if all_workers_with_expired_heartbeats.include?(worker)
612
- log_with_severity :info, "Pruning dead worker: #{worker}"
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
613
629
 
614
- job_class = worker.job(false)['payload']['class'] rescue nil
615
- worker.unregister_worker(PruneDeadWorkerDirtyExit.new(worker.to_s, job_class))
630
+ all_workers.each do |worker|
631
+ if all_workers_with_expired_heartbeats.include?(worker)
616
632
  next
617
633
  end
618
634
 
@@ -681,9 +697,9 @@ module Resque
681
697
 
682
698
  kill_background_threads
683
699
 
684
- data_store.unregister_worker(self) do
685
- Stat.clear("processed:#{self}")
686
- Stat.clear("failed:#{self}")
700
+ data_store.unregister_worker(self) do |**opts|
701
+ Stat.clear("processed:#{self}", **opts)
702
+ Stat.clear("failed:#{self}", **opts)
687
703
  end
688
704
  rescue Exception => exception_while_unregistering
689
705
  message = exception_while_unregistering.message
@@ -704,13 +720,22 @@ module Resque
704
720
  :run_at => Time.now.utc.iso8601,
705
721
  :payload => job.payload
706
722
  data_store.set_worker_payload(self,data)
723
+ state_change
707
724
  end
708
725
 
709
726
  # Called when we are done working - clears our `working_on` state
710
727
  # and tells Redis we processed a job.
711
728
  def done_working
712
- data_store.worker_done_working(self) do
713
- 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
714
739
  end
715
740
  end
716
741
 
@@ -720,9 +745,9 @@ module Resque
720
745
  end
721
746
 
722
747
  # Tell Redis we've processed a job.
723
- def processed!
724
- Stat << "processed"
725
- Stat << "processed:#{self}"
748
+ def processed!(**opts)
749
+ Stat.incr("processed", 1, **opts)
750
+ Stat.incr("processed:#{self}", 1, **opts)
726
751
  end
727
752
 
728
753
  # How many failed jobs has this worker seen? Returns an int.
@@ -836,7 +861,7 @@ module Resque
836
861
  `ps -A -o pid,comm | grep "[r]uby" | grep -v "resque-web"`.split("\n").map do |line|
837
862
  real_pid = line.split(' ')[0]
838
863
  pargs_command = `pargs -a #{real_pid} 2>/dev/null | grep [r]esque | grep -v "resque-web"`
839
- if pargs_command.split(':')[1] == " resque-#{Resque::Version}"
864
+ if pargs_command.split(':')[1] == " resque-#{Resque::VERSION}"
840
865
  real_pid
841
866
  end
842
867
  end.compact
@@ -846,7 +871,7 @@ module Resque
846
871
  # Procline is always in the format of:
847
872
  # RESQUE_PROCLINE_PREFIXresque-VERSION: STRING
848
873
  def procline(string)
849
- $0 = "#{ENV['RESQUE_PROCLINE_PREFIX']}resque-#{Resque::Version}: #{string}"
874
+ $0 = "#{ENV['RESQUE_PROCLINE_PREFIX']}resque-#{Resque::VERSION}: #{string}"
850
875
  log_with_severity :debug, $0
851
876
  end
852
877
 
data/lib/resque.rb CHANGED
@@ -23,6 +23,8 @@ require 'resque/thread_signal'
23
23
 
24
24
  require 'resque/vendor/utf8_util'
25
25
 
26
+ require 'resque/railtie' if defined?(Rails::Railtie)
27
+
26
28
  module Resque
27
29
  include Helpers
28
30
  extend self
@@ -113,12 +115,11 @@ module Resque
113
115
  case server
114
116
  when String
115
117
  if server =~ /rediss?\:\/\//
116
- redis = Redis.new(:url => server, :thread_safe => true)
118
+ redis = Redis.new(:url => server)
117
119
  else
118
120
  server, namespace = server.split('/', 2)
119
121
  host, port, db = server.split(':')
120
- redis = Redis.new(:host => host, :port => port,
121
- :thread_safe => true, :db => db)
122
+ redis = Redis.new(:host => host, :port => port, :db => db)
122
123
  end
123
124
  namespace ||= :resque
124
125
 
@@ -287,6 +288,34 @@ module Resque
287
288
  register_hook(:after_pause, block)
288
289
  end
289
290
 
291
+ # The `queue_empty` hook will be run in the **parent** process when
292
+ # the worker finds no more jobs in the queue and becomes idle.
293
+ #
294
+ # Call with a block to register a hook.
295
+ # Call with no arguments to return all registered hooks.
296
+ def queue_empty(&block)
297
+ block ? register_hook(:queue_empty, block) : hooks(:queue_empty)
298
+ end
299
+
300
+ # Register a queue_empty proc.
301
+ def queue_empty=(block)
302
+ register_hook(:queue_empty, block)
303
+ end
304
+
305
+ # The `worker_exit` hook will be run in the **parent** process
306
+ # after the worker has existed (via SIGQUIT, SIGTERM, SIGINT, etc.).
307
+ #
308
+ # Call with a block to register a hook.
309
+ # Call with no arguments to return all registered hooks.
310
+ def worker_exit(&block)
311
+ block ? register_hook(:worker_exit, block) : hooks(:worker_exit)
312
+ end
313
+
314
+ # Register a worker_exit proc.
315
+ def worker_exit=(block)
316
+ register_hook(:worker_exit, block)
317
+ end
318
+
290
319
  def to_s
291
320
  "Resque Client connected to #{redis_id}"
292
321
  end
@@ -334,8 +363,8 @@ module Resque
334
363
  data_store.queue_size(queue)
335
364
  end
336
365
 
337
- # Returns an array of items currently queued. Queue name should be
338
- # a string.
366
+ # Returns an array of items currently queued, or the item itself
367
+ # if count = 1. Queue name should be a string.
339
368
  #
340
369
  # start and count should be integer and can be used for pagination.
341
370
  # start is the item to begin, count is how many items to return.
@@ -554,9 +583,9 @@ module Resque
554
583
  def queue_sizes
555
584
  queue_names = queues
556
585
 
557
- sizes = redis.pipelined do
586
+ sizes = redis.pipelined do |piped|
558
587
  queue_names.each do |name|
559
- redis.llen("queue:#{name}")
588
+ piped.llen("queue:#{name}")
560
589
  end
561
590
  end
562
591
 
@@ -567,11 +596,11 @@ module Resque
567
596
  def sample_queues(sample_size = 1000)
568
597
  queue_names = queues
569
598
 
570
- samples = redis.pipelined do
599
+ samples = redis.pipelined do |piped|
571
600
  queue_names.each do |name|
572
601
  key = "queue:#{name}"
573
- redis.llen(key)
574
- redis.lrange(key, 0, sample_size - 1)
602
+ piped.llen(key)
603
+ piped.lrange(key, 0, sample_size - 1)
575
604
  end
576
605
  end
577
606