• Jump To … +
    configurator.rb logging.rb lookout.rb master.rb server.rb worker.rb
  • master.rb

  • ¶
    module Spyglass
      class Master
        include Logging
    
        def initialize(connection, socket)
          @connection, @socket = connection, socket
          @worker_pids = []
  • ¶

    The Master shares this pipe with each of its worker processes. It passes the writable end down to each spawned worker while it listens on the readable end. Each worker will write to the pipe each time it accepts a new connection. If The Master doesn't get anything on the pipe before Config.timeout elapses then it kills its workers and exits.

          @readable_pipe, @writable_pipe = IO.pipe
        end
  • ¶

    This method starts the Master. It enters an infinite loop where it creates processes to handle web requests and ensures that they stay active. It takes a connection as an argument from the Lookout instance. A Master will only be started when a connection is received by the Lookout.

        def start
          trap_signals
    
          load_app
          out "Loaded the app"
  • ¶

    The first worker we spawn has to handle the connection that was already passed to us.

          spawn_worker(@connection)
  • ¶

    The Master can now close its handle on the client socket since the forked worker also got a handle on the same socket. Since this one is now closed it's up to the Worker process to close its handle when it's done. At that point the client connection will perceive that it's been closed on their end.

          @connection.close
  • ¶

    We spawn the rest of the workers.

          (Config.workers - 1).times { spawn_worker }
          out "Spawned #{Config.workers} workers. Babysitting now..."
    
          loop do
            if timed_out?(IO.select([@readable_pipe], nil, nil, Config.timeout))
              out "Timed out after #{Config.timeout} s. Exiting."
              
              kill_workers(:QUIT)          
              exit 
            else
  • ¶

    Clear the data on the pipe so it doesn't appear to be readable next time around the loop.

              @readable_pipe.read_nonblock 1
            end
          end
        end
    
        def timed_out?(select_result)
          !select_result
        end
    
        def spawn_worker(connection = nil)
          @worker_pids << fork { Worker.new(@socket, @app, @writable_pipe, connection).start }
        end
    
        def trap_signals
  • ¶

    The QUIT signal triggers a graceful shutdown. The master shuts down immediately and lets each worker finish the request they are currently processing.

          trap(:QUIT) do
            verbose "Received QUIT"
    
            kill_workers(:QUIT)
            exit
          end
    
          trap(:CHLD) do
            dead_worker = Process.wait
            @worker_pids.delete(dead_worker)
    
            @worker_pids.each do |wpid|
              begin 
                dead_worker = Process.waitpid(wpid, Process::WNOHANG)
                @worker_pids.delete(dead_worker)
              rescue Errno::ECHILD
              end
            end
    
            spawn_worker
          end
        end
        
        def kill_workers(sig)
          @worker_pids.each do |wpid|
            Process.kill(sig, wpid)
          end
        end
    
        def load_app
          @app, options = Rack::Builder.parse_file(Config.config_ru_path)
        end
      end
    end