heroku-resque-pool 0.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE.txt +20 -0
- data/README.md +2 -0
- data/Rakefile +37 -0
- data/bin/heroku-resque-pool +7 -0
- data/lib/resque/pool.rb +446 -0
- data/lib/resque/pool/cli.rb +184 -0
- data/lib/resque/pool/file_or_hash_loader.rb +59 -0
- data/lib/resque/pool/killer.rb +40 -0
- data/lib/resque/pool/logging.rb +124 -0
- data/lib/resque/pool/pooled_worker.rb +41 -0
- data/lib/resque/pool/tasks.rb +20 -0
- data/lib/resque/pool/version.rb +5 -0
- data/man/resque-pool.1 +88 -0
- data/man/resque-pool.1.ronn +92 -0
- data/man/resque-pool.yml.5 +46 -0
- data/man/resque-pool.yml.5.ronn +41 -0
- metadata +159 -0
@@ -0,0 +1,184 @@
|
|
1
|
+
require 'optparse'
|
2
|
+
require 'resque/pool'
|
3
|
+
require 'resque/pool/logging'
|
4
|
+
require 'fileutils'
|
5
|
+
|
6
|
+
module Resque
|
7
|
+
class Pool
|
8
|
+
module CLI
|
9
|
+
include Logging
|
10
|
+
extend Logging
|
11
|
+
extend self
|
12
|
+
|
13
|
+
def run
|
14
|
+
opts = parse_options
|
15
|
+
obtain_shared_lock opts[:lock_file]
|
16
|
+
daemonize if opts[:daemon]
|
17
|
+
manage_pidfile opts[:pidfile]
|
18
|
+
redirect opts
|
19
|
+
setup_environment opts
|
20
|
+
set_pool_options opts
|
21
|
+
start_pool
|
22
|
+
end
|
23
|
+
|
24
|
+
def parse_options(argv=nil)
|
25
|
+
opts = {}
|
26
|
+
parser = OptionParser.new do |opt|
|
27
|
+
opt.banner = <<-EOS.gsub(/^ /, '')
|
28
|
+
resque-pool is the best way to manage a group (pool) of resque workers
|
29
|
+
|
30
|
+
When daemonized, stdout and stderr default to resque-pool.stdxxx.log files in
|
31
|
+
the log directory and pidfile defaults to resque-pool.pid in the current dir.
|
32
|
+
|
33
|
+
Usage:
|
34
|
+
resque-pool [options]
|
35
|
+
|
36
|
+
where [options] are:
|
37
|
+
EOS
|
38
|
+
opt.on('-c', '--config PATH', "Alternate path to config file") { |c| opts[:config] = c }
|
39
|
+
opt.on('-a', '--appname NAME', "Alternate appname") { |c| opts[:appname] = c }
|
40
|
+
opt.on("-d", '--daemon', "Run as a background daemon") {
|
41
|
+
opts[:daemon] = true
|
42
|
+
opts[:stdout] ||= "log/resque-pool.stdout.log"
|
43
|
+
opts[:stderr] ||= "log/resque-pool.stderr.log"
|
44
|
+
opts[:pidfile] ||= "tmp/pids/resque-pool.pid" unless opts[:no_pidfile]
|
45
|
+
}
|
46
|
+
opt.on("-k", '--kill-others', "Shutdown any other Resque Pools on startup") { opts[:killothers] = true }
|
47
|
+
opt.on('-o', '--stdout FILE', "Redirect stdout to logfile") { |c| opts[:stdout] = c }
|
48
|
+
opt.on('-e', '--stderr FILE', "Redirect stderr to logfile") { |c| opts[:stderr] = c }
|
49
|
+
opt.on('--nosync', "Don't sync logfiles on every write") { opts[:nosync] = true }
|
50
|
+
opt.on("-p", '--pidfile FILE', "PID file location") { |c|
|
51
|
+
opts[:pidfile] = c
|
52
|
+
opts[:no_pidfile] = false
|
53
|
+
}
|
54
|
+
opt.on('--no-pidfile', "Force no pidfile, even if daemonized") {
|
55
|
+
opts[:pidfile] = nil
|
56
|
+
opts[:no_pidfile] = true
|
57
|
+
}
|
58
|
+
opt.on('-l', '--lock FILE' "Open a shared lock on a file") { |c| opts[:lock_file] = c }
|
59
|
+
opt.on("-H", "--hot-swap", "Set appropriate defaults to hot-swap a new pool for a running pool") {|c|
|
60
|
+
opts[:pidfile] = nil
|
61
|
+
opts[:no_pidfile] = true
|
62
|
+
opts[:lock_file] ||= "tmp/resque-pool.lock"
|
63
|
+
opts[:killothers] = true
|
64
|
+
}
|
65
|
+
opt.on("-E", '--environment ENVIRONMENT', "Set RAILS_ENV/RACK_ENV/RESQUE_ENV") { |c| opts[:environment] = c }
|
66
|
+
opt.on("-s", '--spawn-delay MS', Integer, "Delay in milliseconds between spawning missing workers") { |c| opts[:spawn_delay] = c }
|
67
|
+
opt.on('--term-graceful-wait', "On TERM signal, wait for workers to shut down gracefully") { opts[:term_graceful_wait] = true }
|
68
|
+
opt.on('--term-graceful', "On TERM signal, shut down workers gracefully") { opts[:term_graceful] = true }
|
69
|
+
opt.on('--term-immediate', "On TERM signal, shut down workers immediately (default)") { opts[:term_immediate] = true }
|
70
|
+
opt.on('--single-process-group', "Workers remain in the same process group as the master") { opts[:single_process_group] = true }
|
71
|
+
opt.on("-h", "--help", "Show this.") { puts opt; exit }
|
72
|
+
opt.on("-v", "--version", "Show Version"){ puts "resque-pool #{VERSION} (c) nicholas a. evans"; exit}
|
73
|
+
end
|
74
|
+
parser.parse!(argv || parser.default_argv)
|
75
|
+
|
76
|
+
opts
|
77
|
+
end
|
78
|
+
|
79
|
+
def daemonize
|
80
|
+
raise 'First fork failed' if (pid = fork) == -1
|
81
|
+
exit unless pid.nil?
|
82
|
+
Process.setsid
|
83
|
+
raise 'Second fork failed' if (pid = fork) == -1
|
84
|
+
exit unless pid.nil?
|
85
|
+
end
|
86
|
+
|
87
|
+
# Obtain a lock on a file that will be held for the lifetime of
|
88
|
+
# the process. This aids in concurrent daemonized deployment with
|
89
|
+
# process managers like upstart since multiple pools can share a
|
90
|
+
# lock, but not a pidfile.
|
91
|
+
def obtain_shared_lock(lock_path)
|
92
|
+
return unless lock_path
|
93
|
+
@lock_file = File.open(lock_path, 'w')
|
94
|
+
unless @lock_file.flock(File::LOCK_SH)
|
95
|
+
fail "unable to obtain shared lock on #{@lock_file}"
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def manage_pidfile(pidfile)
|
100
|
+
return unless pidfile
|
101
|
+
pid = Process.pid
|
102
|
+
if File.exist? pidfile
|
103
|
+
if process_still_running? pidfile
|
104
|
+
raise "Pidfile already exists at #{pidfile} and process is still running."
|
105
|
+
else
|
106
|
+
File.delete pidfile
|
107
|
+
end
|
108
|
+
else
|
109
|
+
FileUtils.mkdir_p File.dirname(pidfile)
|
110
|
+
end
|
111
|
+
File.open pidfile, "w" do |f|
|
112
|
+
f.write pid
|
113
|
+
end
|
114
|
+
at_exit do
|
115
|
+
if Process.pid == pid
|
116
|
+
File.delete pidfile
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
def process_still_running?(pidfile)
|
122
|
+
old_pid = open(pidfile).read.strip.to_i
|
123
|
+
old_pid > 0 && Process.kill(0, old_pid)
|
124
|
+
rescue Errno::ESRCH
|
125
|
+
false
|
126
|
+
rescue Errno::EPERM
|
127
|
+
true
|
128
|
+
rescue ::Exception => e
|
129
|
+
$stderr.puts "While checking if PID #{old_pid} is running, unexpected #{e.class}: #{e}"
|
130
|
+
true
|
131
|
+
end
|
132
|
+
|
133
|
+
def redirect(opts)
|
134
|
+
$stdin.reopen '/dev/null' if opts[:daemon]
|
135
|
+
# need to reopen as File, or else Resque::Pool::Logging.reopen_logs! won't work
|
136
|
+
out = File.new(opts[:stdout], "a") if opts[:stdout] && !opts[:stdout].empty?
|
137
|
+
err = File.new(opts[:stderr], "a") if opts[:stderr] && !opts[:stderr].empty?
|
138
|
+
$stdout.reopen out if out
|
139
|
+
$stderr.reopen err if err
|
140
|
+
$stdout.sync = $stderr.sync = true unless opts[:nosync]
|
141
|
+
end
|
142
|
+
|
143
|
+
# TODO: global variables are not the best way
|
144
|
+
def set_pool_options(opts)
|
145
|
+
if opts[:daemon]
|
146
|
+
Resque::Pool.handle_winch = true
|
147
|
+
end
|
148
|
+
if opts[:term_graceful_wait]
|
149
|
+
Resque::Pool.term_behavior = "graceful_worker_shutdown_and_wait"
|
150
|
+
elsif opts[:term_graceful]
|
151
|
+
Resque::Pool.term_behavior = "graceful_worker_shutdown"
|
152
|
+
elsif ENV["TERM_CHILD"]
|
153
|
+
log "TERM_CHILD enabled, so will user 'term-graceful-and-wait' behaviour"
|
154
|
+
Resque::Pool.term_behavior = "graceful_worker_shutdown_and_wait"
|
155
|
+
end
|
156
|
+
if ENV.include?("DYNO") && !ENV["TERM_CHILD"]
|
157
|
+
log "WARNING: Are you running on Heroku? You should probably set TERM_CHILD=1"
|
158
|
+
end
|
159
|
+
if opts[:spawn_delay]
|
160
|
+
Resque::Pool.spawn_delay = opts[:spawn_delay] * 0.001
|
161
|
+
end
|
162
|
+
Resque::Pool.kill_other_pools = !!opts[:killothers]
|
163
|
+
end
|
164
|
+
|
165
|
+
def setup_environment(opts)
|
166
|
+
Resque::Pool.app_name = opts[:appname] if opts[:appname]
|
167
|
+
ENV["RACK_ENV"] = ENV["RAILS_ENV"] = ENV["RESQUE_ENV"] = opts[:environment] if opts[:environment]
|
168
|
+
Resque::Pool.log "Resque Pool running in #{ENV["RAILS_ENV"] || "development"} environment"
|
169
|
+
ENV["RESQUE_POOL_CONFIG"] = opts[:config] if opts[:config]
|
170
|
+
Resque::Pool.single_process_group = opts[:single_process_group]
|
171
|
+
end
|
172
|
+
|
173
|
+
def start_pool
|
174
|
+
require 'rake'
|
175
|
+
require 'resque/pool/tasks'
|
176
|
+
Rake.application.init
|
177
|
+
Rake.application.load_rakefile
|
178
|
+
Rake.application["resque:pool"].invoke
|
179
|
+
end
|
180
|
+
|
181
|
+
end
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
@@ -0,0 +1,59 @@
|
|
1
|
+
module Resque
|
2
|
+
class Pool
|
3
|
+
class FileOrHashLoader
|
4
|
+
def initialize(filename_or_hash=nil)
|
5
|
+
case filename_or_hash
|
6
|
+
when String, nil
|
7
|
+
@filename = filename_or_hash
|
8
|
+
when Hash
|
9
|
+
@static_config = filename_or_hash.dup
|
10
|
+
else
|
11
|
+
raise "#{self.class} cannot be initialized with #{filename_or_hash.inspect}"
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def call(environment)
|
16
|
+
@config ||= load_config_from_file(environment)
|
17
|
+
end
|
18
|
+
|
19
|
+
def reset!
|
20
|
+
@config = nil
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def load_config_from_file(environment)
|
26
|
+
if @static_config
|
27
|
+
new_config = @static_config
|
28
|
+
else
|
29
|
+
filename = config_filename
|
30
|
+
new_config = load_config filename
|
31
|
+
end
|
32
|
+
apply_environment new_config, environment
|
33
|
+
end
|
34
|
+
|
35
|
+
def apply_environment(config, environment)
|
36
|
+
environment and config[environment] and config.merge!(config[environment])
|
37
|
+
config.delete_if {|key, value| value.is_a? Hash }
|
38
|
+
end
|
39
|
+
|
40
|
+
def config_filename
|
41
|
+
@filename || choose_config_file
|
42
|
+
end
|
43
|
+
|
44
|
+
def load_config(filename)
|
45
|
+
return {} unless filename
|
46
|
+
YAML.load(ERB.new(IO.read(filename)).result)
|
47
|
+
end
|
48
|
+
|
49
|
+
CONFIG_FILES = ["resque-pool.yml", "config/resque-pool.yml"]
|
50
|
+
def choose_config_file
|
51
|
+
if ENV["RESQUE_POOL_CONFIG"]
|
52
|
+
ENV["RESQUE_POOL_CONFIG"]
|
53
|
+
else
|
54
|
+
CONFIG_FILES.detect { |f| File.exist?(f) }
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module Resque
|
2
|
+
class Pool
|
3
|
+
class Killer
|
4
|
+
include Logging
|
5
|
+
|
6
|
+
GRACEFUL_SHUTDOWN_SIGNAL=:INT
|
7
|
+
|
8
|
+
def self.run
|
9
|
+
new.run
|
10
|
+
end
|
11
|
+
|
12
|
+
def run
|
13
|
+
my_pid = Process.pid
|
14
|
+
pool_pids = all_resque_pool_processes
|
15
|
+
pids_to_kill = pool_pids.reject{|pid| pid == my_pid}
|
16
|
+
pids_to_kill.each do |pid|
|
17
|
+
log "Pool (#{my_pid}) in kill-others mode: killing pool with pid (#{pid})"
|
18
|
+
Process.kill(GRACEFUL_SHUTDOWN_SIGNAL, pid)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
|
23
|
+
def all_resque_pool_processes
|
24
|
+
out = `ps -e -o pid= -o command= 2>&1`
|
25
|
+
raise "Unable to identify other pools: #{out}" unless $?.success?
|
26
|
+
parse_pids_from_output out
|
27
|
+
end
|
28
|
+
|
29
|
+
RESQUE_POOL_PIDS = /
|
30
|
+
^\s*(\d+) # PID digits, optional leading spaces
|
31
|
+
\s+ # column divider
|
32
|
+
#{Regexp.escape(PROCLINE_PREFIX)} # exact match at start of command
|
33
|
+
/x
|
34
|
+
|
35
|
+
def parse_pids_from_output(output)
|
36
|
+
output.scan(RESQUE_POOL_PIDS).flatten.map(&:to_i)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,124 @@
|
|
1
|
+
module Resque
|
2
|
+
class Pool
|
3
|
+
module Logging
|
4
|
+
extend self
|
5
|
+
|
6
|
+
# This reopens ALL logfiles in the process that have been rotated
|
7
|
+
# using logrotate(8) (without copytruncate) or similar tools.
|
8
|
+
# A +File+ object is considered for reopening if it is:
|
9
|
+
# 1) opened with the O_APPEND and O_WRONLY flags
|
10
|
+
# 2) the current open file handle does not match its original open path
|
11
|
+
# 3) unbuffered (as far as userspace buffering goes, not O_SYNC)
|
12
|
+
# Returns the number of files reopened
|
13
|
+
#
|
14
|
+
# This was mostly copied from Unicorn 4.8.2 to simplify reopening
|
15
|
+
# logs in the same way that Unicorn does. Original comments and
|
16
|
+
# explanations are left intact.
|
17
|
+
def self.reopen_logs!
|
18
|
+
to_reopen = [ ]
|
19
|
+
reopened_count = 0
|
20
|
+
|
21
|
+
ObjectSpace.each_object(File) { |fp| is_log?(fp) and to_reopen << fp }
|
22
|
+
log "Flushing #{to_reopen.length} logs"
|
23
|
+
|
24
|
+
to_reopen.each do |fp|
|
25
|
+
orig_st = begin
|
26
|
+
fp.stat
|
27
|
+
rescue IOError, Errno::EBADF # race
|
28
|
+
next
|
29
|
+
end
|
30
|
+
|
31
|
+
begin
|
32
|
+
b = File.stat(fp.path)
|
33
|
+
# Skip if reopening wouldn't do anything
|
34
|
+
next if orig_st.ino == b.ino && orig_st.dev == b.dev
|
35
|
+
rescue Errno::ENOENT
|
36
|
+
end
|
37
|
+
|
38
|
+
begin
|
39
|
+
# stdin, stdout, stderr are special. The following dance should
|
40
|
+
# guarantee there is no window where `fp' is unwritable in MRI
|
41
|
+
# (or any correct Ruby implementation).
|
42
|
+
#
|
43
|
+
# Fwiw, GVL has zero bearing here. This is tricky because of
|
44
|
+
# the unavoidable existence of stdio FILE * pointers for
|
45
|
+
# std{in,out,err} in all programs which may use the standard C library
|
46
|
+
if fp.fileno <= 2
|
47
|
+
# We do not want to hit fclose(3)->dup(2) window for std{in,out,err}
|
48
|
+
# MRI will use freopen(3) here internally on std{in,out,err}
|
49
|
+
fp.reopen(fp.path, "a")
|
50
|
+
else
|
51
|
+
# We should not need this workaround, Ruby can be fixed:
|
52
|
+
# http://bugs.ruby-lang.org/issues/9036
|
53
|
+
# MRI will not call call fclose(3) or freopen(3) here
|
54
|
+
# since there's no associated std{in,out,err} FILE * pointer
|
55
|
+
# This should atomically use dup3(2) (or dup2(2)) syscall
|
56
|
+
File.open(fp.path, "a") { |tmpfp| fp.reopen(tmpfp) }
|
57
|
+
end
|
58
|
+
|
59
|
+
fp.sync = true
|
60
|
+
fp.flush # IO#sync=true may not implicitly flush
|
61
|
+
new_st = fp.stat
|
62
|
+
|
63
|
+
# this should only happen in the master:
|
64
|
+
if orig_st.uid != new_st.uid || orig_st.gid != new_st.gid
|
65
|
+
fp.chown(orig_st.uid, orig_st.gid)
|
66
|
+
end
|
67
|
+
|
68
|
+
log "Reopened logfile: #{fp.path}"
|
69
|
+
reopened_count += 1
|
70
|
+
rescue IOError, Errno::EBADF
|
71
|
+
# not much we can do...
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
reopened_count
|
76
|
+
end
|
77
|
+
|
78
|
+
PROCLINE_PREFIX="resque-pool-master"
|
79
|
+
|
80
|
+
# Given a string, sets the procline ($0)
|
81
|
+
# Procline is always in the format of:
|
82
|
+
# resque-pool-master: STRING
|
83
|
+
def procline(string)
|
84
|
+
$0 = "#{PROCLINE_PREFIX}#{app}: #{string}"
|
85
|
+
end
|
86
|
+
|
87
|
+
# TODO: make this use an actual logger
|
88
|
+
def log(message)
|
89
|
+
return if $skip_logging
|
90
|
+
puts "resque-pool-manager#{app}[#{Process.pid}]: #{message}"
|
91
|
+
#$stdout.fsync
|
92
|
+
end
|
93
|
+
|
94
|
+
# TODO: make this use an actual logger
|
95
|
+
def log_worker(message)
|
96
|
+
return if $skip_logging
|
97
|
+
puts "resque-pool-worker#{app}[#{Process.pid}]: #{message}"
|
98
|
+
#$stdout.fsync
|
99
|
+
end
|
100
|
+
|
101
|
+
# Include optional app name in procline
|
102
|
+
def app
|
103
|
+
app_name = self.respond_to?(:app_name) && self.app_name
|
104
|
+
app_name ||= self.class.respond_to?(:app_name) && self.class.app_name
|
105
|
+
app_name ? "[#{app_name}]" : ""
|
106
|
+
end
|
107
|
+
|
108
|
+
private
|
109
|
+
|
110
|
+
# Used by reopen_logs, borrowed from Unicorn...
|
111
|
+
def self.is_log?(fp)
|
112
|
+
append_flags = File::WRONLY | File::APPEND
|
113
|
+
|
114
|
+
! fp.closed? &&
|
115
|
+
fp.stat.file? &&
|
116
|
+
fp.sync &&
|
117
|
+
(fp.fcntl(Fcntl::F_GETFL) & append_flags) == append_flags
|
118
|
+
rescue IOError, Errno::EBADF
|
119
|
+
false
|
120
|
+
end
|
121
|
+
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require 'resque/worker'
|
2
|
+
|
3
|
+
class Resque::Pool
|
4
|
+
module PooledWorker
|
5
|
+
attr_accessor :pool_master_pid
|
6
|
+
attr_accessor :worker_parent_pid
|
7
|
+
|
8
|
+
# We will return false if there are no potential_parent_pids, because that
|
9
|
+
# means we aren't even running inside resque-pool.
|
10
|
+
#
|
11
|
+
# We can't just check if we've been re-parented to PID 1 (init) because we
|
12
|
+
# want to support docker (which will make the pool master PID 1).
|
13
|
+
#
|
14
|
+
# We also check the worker_parent_pid, because resque-multi-jobs-fork calls
|
15
|
+
# Worker#shutdown? from inside the worker child process.
|
16
|
+
def pool_master_has_gone_away?
|
17
|
+
pids = potential_parent_pids
|
18
|
+
pids.any? && !pids.include?(Process.ppid)
|
19
|
+
end
|
20
|
+
|
21
|
+
def potential_parent_pids
|
22
|
+
[pool_master_pid, worker_parent_pid].compact
|
23
|
+
end
|
24
|
+
|
25
|
+
def shutdown_with_pool?
|
26
|
+
shutdown_without_pool? || pool_master_has_gone_away?
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.included(base)
|
30
|
+
base.instance_eval do
|
31
|
+
alias_method :shutdown_without_pool?, :shutdown?
|
32
|
+
alias_method :shutdown?, :shutdown_with_pool?
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
Resque::Worker.class_eval do
|
40
|
+
include Resque::Pool::PooledWorker
|
41
|
+
end
|