discourse/lib/demon/base.rb
Sam Saffron 30990006a9 DEV: enable frozen string literal on all files
This reduces chances of errors where consumers of strings mutate inputs
and reduces memory usage of the app.

Test suite passes now, but there may be some stuff left, so we will run
a few sites on a branch prior to merging
2019-05-13 09:31:32 +08:00

226 lines
4.0 KiB
Ruby

# frozen_string_literal: true
module Demon; end
# intelligent fork based demonizer
class Demon::Base
def self.demons
@demons
end
def self.start(count = 1, verbose: false)
@demons ||= {}
count.times do |i|
(@demons["#{prefix}_#{i}"] ||= new(i, verbose: verbose)).start
end
end
def self.stop
return unless @demons
@demons.values.each do |demon|
demon.stop
end
end
def self.restart
return unless @demons
@demons.values.each do |demon|
demon.stop
demon.start
end
end
def self.ensure_running
@demons.values.each do |demon|
demon.ensure_running
end
end
attr_reader :pid, :parent_pid, :started, :index
attr_accessor :stop_timeout
def initialize(index, rails_root: nil, parent_pid: nil, verbose: false)
@index = index
@pid = nil
@parent_pid = parent_pid || Process.pid
@started = false
@stop_timeout = 10
@rails_root = rails_root || Rails.root
@verbose = verbose
end
def pid_file
"#{@rails_root}/tmp/pids/#{self.class.prefix}_#{@index}.pid"
end
def alive?(pid = nil)
pid ||= @pid
if pid
Demon::Base.alive?(pid)
else
false
end
end
def stop_signal
"HUP"
end
def stop
@started = false
if @pid
Process.kill(stop_signal, @pid)
wait_for_stop = lambda {
timeout = @stop_timeout
while alive? && timeout > 0
timeout -= (@stop_timeout / 10.0)
sleep(@stop_timeout / 10.0)
Process.waitpid(@pid, Process::WNOHANG) rescue -1
end
Process.waitpid(@pid, Process::WNOHANG) rescue -1
}
wait_for_stop.call
if alive?
STDERR.puts "Process would not terminate cleanly, force quitting. pid: #{@pid} #{self.class}"
Process.kill("KILL", @pid)
end
wait_for_stop.call
@pid = nil
@started = false
end
end
def ensure_running
return unless @started
if !@pid
@started = false
start
return
end
dead = Process.waitpid(@pid, Process::WNOHANG) rescue -1
if dead
STDERR.puts "Detected dead worker #{@pid}, restarting..."
@pid = nil
@started = false
start
end
end
def start
return if @pid || @started
if existing = already_running?
# should not happen ... so kill violently
STDERR.puts "Attempting to kill pid #{existing}"
Process.kill("TERM", existing)
end
@started = true
run
end
def run
if @pid = fork
write_pid_file
return
end
monitor_parent
establish_app
after_fork
end
def already_running?
if File.exists? pid_file
pid = File.read(pid_file).to_i
if Demon::Base.alive?(pid)
return pid
end
end
nil
end
def self.alive?(pid)
Process.kill(0, pid)
true
rescue
false
end
private
def verbose(msg)
if @verbose
puts msg
end
end
def write_pid_file
verbose("writing pid file #{pid_file} for #{@pid}")
FileUtils.mkdir_p(@rails_root + "tmp/pids")
File.open(pid_file, 'w') do |f|
f.write(@pid)
end
end
def delete_pid_file
File.delete(pid_file)
end
def monitor_parent
Thread.new do
while true
begin
unless alive?(@parent_pid)
Process.kill "TERM", Process.pid
sleep 10
Process.kill "KILL", Process.pid
end
rescue => e
STDERR.puts "URGENT monitoring thread had an exception #{e}"
end
sleep 1
end
end
end
def suppress_stdout
true
end
def suppress_stderr
true
end
def establish_app
Discourse.after_fork if defined?(Discourse)
Signal.trap("HUP") do
begin
delete_pid_file
ensure
# TERM is way cleaner than exit
Process.kill("TERM", Process.pid)
end
end
# keep stuff simple for now
$stdout.reopen("/dev/null", "w") if suppress_stdout
$stderr.reopen("/dev/null", "w") if suppress_stderr
end
def after_fork
end
end