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.
@@ -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