heroku-resque-pool 0.0.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.
- 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
|