resque 2.0.0 → 2.2.1

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,372 @@
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 (namespace = options[:redis_namespace])
89
+ runner.logger.info "Using Redis namespace '#{namespace}'"
90
+ Resque.redis.namespace = namespace
91
+ end
92
+ if (redis_conf = options[:redis_conf])
93
+ logger.info "Using Redis connection '#{redis_conf}'"
94
+ Resque.redis = redis_conf
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.exists?(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.exists?(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 => e
290
+ next
291
+ end
292
+ end
293
+ handler
294
+
295
+ # :server might be set explicitly to a single option like "mongrel"
296
+ else
297
+ Rack::Handler.get(@app.server)
298
+ end
299
+
300
+ # If all else fails, we'll use Thin
301
+ else
302
+ JRUBY ? Rack::Handler::WEBrick : Rack::Handler::Thin
303
+ end
304
+ end
305
+
306
+ def load_options(runtime_args)
307
+ @args = option_parser.parse!(runtime_args)
308
+ options.merge!(option_parser.options)
309
+ args
310
+ rescue OptionParser::MissingArgument => e
311
+ logger.warn "#{e}, run -h for options"
312
+ exit
313
+ end
314
+
315
+ def option_parser
316
+ @option_parser ||= Parser.new(app_name)
317
+ end
318
+
319
+ class Parser < OptionParser
320
+ attr_reader :command, :options
321
+
322
+ def initialize(app_name)
323
+ super("", 24, ' ')
324
+ self.banner = "Usage: #{app_name} [options]"
325
+
326
+ @options = {}
327
+ basename = app_name.gsub(/\W+/, "_")
328
+ on('-K', "--kill", "kill the running process and exit") { @command = :kill }
329
+ on('-S', "--status", "display the current running PID and URL then quit") { @command = :status }
330
+ string_option("-s", "--server SERVER", "serve using SERVER (thin/mongrel/webrick)", :rack_handler)
331
+ string_option("-o", "--host HOST", "listen on HOST (default: #{HOST})", :host)
332
+ string_option("-p", "--port PORT", "use PORT (default: #{PORT})", :port)
333
+ on("-x", "--no-proxy", "ignore env proxy settings (e.g. http_proxy)") { opts[:no_proxy] = true }
334
+ boolean_option("-F", "--foreground", "don't daemonize, run in the foreground", :foreground)
335
+ boolean_option("-L", "--no-launch", "don't launch the browser", :skip_launch)
336
+ boolean_option('-d', "--debug", "raise the log level to :debug (default: :info)", :debug)
337
+ string_option("--app-dir APP_DIR", "set the app dir where files are stored (default: ~/#{basename}/)", :app_dir)
338
+ string_option("-P", "--pid-file PID_FILE", "set the path to the pid file (default: app_dir/#{basename}.pid)", :pid_file)
339
+ string_option("--log-file LOG_FILE", "set the path to the log file (default: app_dir/#{basename}.log)", :log_file)
340
+ string_option("--url-file URL_FILE", "set the path to the URL file (default: app_dir/#{basename}.url)", :url_file)
341
+ string_option('-N NAMESPACE', "--namespace NAMESPACE", "set the Redis namespace", :redis_namespace)
342
+ string_option('-r redis-connection', "--redis redis-connection", "set the Redis connection string", :redis_conf)
343
+ string_option('-a url-prefix', "--append url-prefix", "set reverse_proxy friendly prefix to links", :url_prefix)
344
+ separator ""
345
+ separator "Common options:"
346
+ on_tail("-h", "--help", "Show this message") { @command = :help }
347
+ on_tail("--version", "Show version") { @command = :version }
348
+ end
349
+
350
+ def boolean_option(*argv)
351
+ k = argv.pop; on(*argv) { options[k] = true }
352
+ end
353
+
354
+ def string_option(*argv)
355
+ k = argv.pop; on(*argv) { |value| options[k] = value }
356
+ end
357
+ end
358
+
359
+ def kill_commands
360
+ WINDOWS ? [1] : [:INT, :TERM]
361
+ end
362
+
363
+ def kill_command
364
+ kill_commands[0]
365
+ end
366
+
367
+ def delete_pid!
368
+ File.delete(pid_file) if File.exist?(pid_file)
369
+ end
370
+ end
371
+
372
+ 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
 
@@ -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
 
@@ -704,6 +720,7 @@ 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
@@ -714,6 +731,14 @@ module Resque
714
731
  end
715
732
  end
716
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
739
+ end
740
+ end
741
+
717
742
  # How many jobs has this worker processed? Returns an int.
718
743
  def processed
719
744
  Stat["processed:#{self}"]
@@ -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
@@ -287,6 +289,34 @@ module Resque
287
289
  register_hook(:after_pause, block)
288
290
  end
289
291
 
292
+ # The `queue_empty` hook will be run in the **parent** process when
293
+ # the worker finds no more jobs in the queue and becomes idle.
294
+ #
295
+ # Call with a block to register a hook.
296
+ # Call with no arguments to return all registered hooks.
297
+ def queue_empty(&block)
298
+ block ? register_hook(:queue_empty, block) : hooks(:queue_empty)
299
+ end
300
+
301
+ # Register a queue_empty proc.
302
+ def queue_empty=(block)
303
+ register_hook(:queue_empty, block)
304
+ end
305
+
306
+ # The `worker_exit` hook will be run in the **parent** process
307
+ # after the worker has existed (via SIGQUIT, SIGTERM, SIGINT, etc.).
308
+ #
309
+ # Call with a block to register a hook.
310
+ # Call with no arguments to return all registered hooks.
311
+ def worker_exit(&block)
312
+ block ? register_hook(:worker_exit, block) : hooks(:worker_exit)
313
+ end
314
+
315
+ # Register a worker_exit proc.
316
+ def worker_exit=(block)
317
+ register_hook(:worker_exit, block)
318
+ end
319
+
290
320
  def to_s
291
321
  "Resque Client connected to #{redis_id}"
292
322
  end
@@ -334,8 +364,8 @@ module Resque
334
364
  data_store.queue_size(queue)
335
365
  end
336
366
 
337
- # Returns an array of items currently queued. Queue name should be
338
- # a string.
367
+ # Returns an array of items currently queued, or the item itself
368
+ # if count = 1. Queue name should be a string.
339
369
  #
340
370
  # start and count should be integer and can be used for pagination.
341
371
  # start is the item to begin, count is how many items to return.