Detach process

This is part of the Semicolon&Sons Code Diary - consisting of lessons learned on the job. You're in the unix category.

Last Updated: 2022-05-26

What does the 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.

So talk me through how this applies to a complex piece of system code?

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.

Resources