This is part of the Semicolon&Sons Code Diary - consisting of lessons learned on the job. You're in the unix category.
Last Updated: 2024-11-21
Process.detach(pid)
do?In a sentence: "Sets up a separate Ruby thread to clear up pid after it terminates, preventing zombie processes."
Some operating systems retain the status of terminated child processes until the
parent collects that status (normally using some variant of wait()
). If the
parent never collects this status, the child stays around as a zombie process.
Process::detach
prevents this by setting up a separate Ruby thread whose sole
job is to reap the status of the process pid when it terminates.
# It's a combination of `pthread_create` and `wait`
Thread.new { Process.wait(pid) }
Use detach only when you do not intend to explicitly wait for the child to terminate
For an example of the issue
p1 = fork { sleep 0.1 }
p2 = fork { sleep 0.2 }
Process.waitpid(p2)
sleep 2
system("ps -ho pid,state -p #{p1}")
This call to ps
will show a process name in brackets (e.g. (ps)
instead of
ps
) with the status Z
(i.e. Zombie) because p1 was not waited upon.
child, child_socket = UNIXSocket.pair
pid = Process.spawn(
{
"RAILS_ENV" => app_env,
"RACK_ENV" => app_env,
"SPRING_ORIGINAL_ENV" => JSON.dump(Spring::ORIGINAL_ENV),
"SPRING_PRELOAD" => preload ? "1" : "0"
},
"ruby",
"-I", File.expand_path("../..", $LOADED_FEATURES.grep(/bundler\/setup\.rb$/).first),
"-I", File.expand_path("../..", __FILE__),
"-e", "require 'spring/application/boot'",
3 => child_socket,
4 => spring_env.log_file,
)
def start_wait_thread(pid, child)
Process.detach(pid)
# This was defined as being a thread which rescues any exceptions that occur
# within so they don't bubble up
Spring.failsafe_thread {
# The `child.recv` can raise an ECONNRESET, killing the thread, but that's ok
# as if it does we're no longer interested in the child
loop do
# select() allows a program to monitor multiple file descriptors, waiting until
# one or more of the file descriptors become "ready" for some class of I/O
# operation (e.g., input possible). A file descriptor is considered ready
# if a (e.g. read) call will not block.
# Thus it blocks until the descriptor is ready (or the call is interrupted
# by a signal handler.
IO.select([child])
break if child.recv(1, Socket::MSG_PEEK).empty?
sleep 0.01
end
log "child #{pid} shutdown"
synchronize {
if @pid == pid
@pid = nil
restart
end
}
}
end
start_wait_thread(pid, child) if child.gets
child_socket.close
Two sockets are created with UnixSocket.pair
. The naming is a tad weird IMO,
but basically if you do child.send("some text", 0)
then you can receive that
message on child_socket.recv(num_of_chars)
. It looks like the sockets are used as a
way to communicate across processes.
Process.spawn
runs ruby ... -e spring/application/boot
in a subprocess with
this socket, but unlike Kernel.system
, spawn
returns immediately instead of waiting for
the subprocess to finish. Therefore instead of returning the output it returns
the pid
.
child.gets
blocks at least if you call it immediately after UnixSocket.pair
.
So something in the start_wait_thread
must signal it.