Ruby code that opens a pipe to send data from a forked process to its parent and cleans up zombie threads

On and off, I’ve been working on a piece of Ruby code that uses phantomjs to automate web requests. Under certain situations, and heavy concurrent requests, it’s been known to leave behind zombie threads. In a previous blog post I shared some code that checks for zombie child threads and spawns a new process to remove them. For this particular scenario, the code was wrapped in Sinatra and served via Thin to provide web services. Restarting the parent process (in this case, Thin) was clearly unacceptable, so here is an alternative. The following code opens an IO pipe (which is shared with child processes), forks, executes the code, serializes the result, and writes it to the pipe. The parent process waits for it to finish, reads the result form the pipe, and checks for child processes left behind. If found, they are detached (assuming a parent pid of 1) and then killed.

#!/usr/bin/env ruby

class PipeForker

  def self.main

    # ensure block was given
    raise "Block required" unless block_given?

    # open IO pipe
    reader, writer = IO.pipe

    # fork child process to execute code
    child_pid = Process.fork do

      # close reader, not needed
      reader.close

      # do code
      result = nil
      begin
        result = yield
      rescue
      end

      # serialize response, write to pipe
      writer.write Marshal.dump result

    end

    # wait for child process to finish
    Process.waitpid child_pid

    # get response from io pipe
    writer.close
    result = Marshal.load reader.gets

    # check if child processes still exist, potentially zombie
    output = `ps -eo pid,ppid,comm | grep #{child_pid}`
    output = output.split("\n").reverse
    output.each do |s|

      # use regex to split up pid, ppid, comm
      matches = /^\s*(\d+)\s+(\d+)\s+(.*)$/.match s

      # check if child processes are still running
      if matches[1].to_i == child_pid || matches[2].to_i == child_pid
        # detach and remove
        Process.detach matches[1].to_i
        Process.kill 9, matches[1].to_i
      end

    end

    # debug
    puts result

    result

  end

end

# sample execution:
PipeForker.main { "do something crazy" }

Updated: