heroku-resque-pool 0.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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