Class: SCGI::Processor

Inherits:
Monitor
  • Object
show all
Defined in:
lib/scgi.rb

Overview

This is the complete guts of the SCGI system. It is designed so that people can take it and implement it for their own systems, not just Ruby on Rails. This implementation is not complete since you must create your own that implements the process_request method.

The SCGI protocol only works with TCP/IP sockets and not domain sockets. It might be useful for shared hosting people to have domain sockets, but they aren’t supported in Apache, and in lighttpd they’re unreliable. Also, domain sockets don’t work so well on Windows.

Instance Method Summary collapse

Constructor Details

#initialize(settings = {}) ⇒ Processor



85
86
87
88
89
90
91
92
93
94
# File 'lib/scgi.rb', line 85

def initialize(settings = {})
  @total_conns = 0
  @shutdown = false
  @dead = false
  @threads = Queue.new
  @log = LogFactory.instance.create(settings[:logfile] || 'log/scgi.log')
  @maxconns = settings[:maxconns] || 2**30-1
  super()
  setup_signals
end

Instance Method Details

#collect_thread(thread) ⇒ Object



125
126
127
128
129
130
131
132
133
134
135
# File 'lib/scgi.rb', line 125

def collect_thread(thread)
  begin
    thread.join
  rescue Interrupt
    @log.info("Shutting down from SIGINT.")
  rescue IOError
    @log.error("received IOError #$!.  Web server may possibly be configured wrong.")
  rescue Object
    @log.error("Collecting thread", $!)
  end
end

#handle_client(socket) ⇒ Object

Internal function that handles a new client connection. It spawns a thread to handle the client and registers it in the and clearing them out. This design is needed because Ruby’s GC doesn’t seem to deal with threads as well as others believe.

Depending on how your system works, you may need to synchronize inside your process_request implementation. scgi_rails.rb does this so that Rails will run as if it were single threaded.

It also handles calculating the current and total connections, and deals with the graceful shutdown. The important part of graceful shutdown is that new requests get redirected to the /busy.html file.



152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
# File 'lib/scgi.rb', line 152

def handle_client(socket)
  # ruby's GC seems to do weird things if we don't assign the thread to a local variable
  @threads << Thread.new do
    begin
      len = ""
      # we only read 10 bytes of the length.  any request longer than this is invalid
      while len.length <= 10
        c = socket.read(1)
        break if c == ':' # found the terminal, len now has a length in it so read the payload
        len << c
      end
      
      # we should now either have a payload length to get
      payload = socket.read(len.to_i)
      if socket.read(1) == ','
        read_header(socket, payload)
      else
        @log.error("Malformed request, does not end with ','")
      end
    rescue Object
      @log.error("Handling client", $!)
    ensure
      # no matter what we have to put this thread on the bad list
      socket.close if not socket.closed?
    end
  end
end

#listen(socket) ⇒ Object

Starts the SCGI::Processor having it listen on the given socket. This function does not return until a shutdown.



98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
# File 'lib/scgi.rb', line 98

def listen(socket)
  @socket = socket
  
  # we also need a small collector thread that does nothing
  # but pull threads off the thread queue and joins them
  @collector = Thread.new do
    while t = @threads.shift
      collect_thread(t)
      @total_conns += 1
    end
  end
    
  thread = Thread.new do
    loop do
      handle_client(@socket.accept)
      break if @shutdown and @threads.length <= 0
    end
  end
    
  # and then collect the listener thread which blocks until it exits
  collect_thread(thread)
  
  @socket.close unless @socket.closed?
  @dead = true
  @log.info("Exited accept loop. Shutdown complete.")
end

#process_request(request, body, socket) ⇒ Object

You must implement this yourself. The request is a Hash of the CGI parameters from the webserver. The body is the raw CGI body. The socket is where you write your results (properly HTTP formatted) back to the webserver.



208
209
210
# File 'lib/scgi.rb', line 208

def process_request(request, body, socket)
  raise "You must implement process_request"
end

#read_header(socket, payload) ⇒ Object

Does three jobs: reads and parses the SCGI netstring header, reads any content off the socket, and then either calls process_request or immediately returns a redirect to /busy.html for some connections.

The browser/connection that will be redirected to /busy.html if either SCGI::Processor is in the middle of a shutdown, or if the number of connections is over the @maxconns. This redirect is immediate and doesn’t run inside the interpreter, so it will happen with much less processing and help keep your system responsive.



189
190
191
192
193
194
195
196
197
198
199
200
201
202
# File 'lib/scgi.rb', line 189

def read_header(socket, payload)
  return if socket.closed?
  request = Hash[*(payload.split("\0"))]
  if request["CONTENT_LENGTH"]
    length = request["CONTENT_LENGTH"].to_i
    body = length > 0 ? socket.read(length) : ''
    
    if @shutdown or @threads.length > @maxconns
      socket.write("Location: /busy.html\r\nCache-control: no-cache, must-revalidate\r\nExpires: Mon, 26 Jul 1997 05:00:00 GMT\r\nStatus: 307 Temporary Redirect\r\n\r\n")
    else
      process_request(request, body, socket)
    end
  end
end

#setup_signalsObject

Sets up the POSIX signals:

  • TERM – Forced shutdown.

  • INT – Graceful shutdown.

  • HUP – Graceful shutdown.

  • USR2 – Dumps status info to the logs. Super ugly.



218
219
220
221
222
223
# File 'lib/scgi.rb', line 218

def setup_signals
  trap("TERM") { @log.info("SIGTERM, forced shutdown."); shutdown(force=true) }
  trap("INT") { @log.info("SIGINT, graceful shutdown started."); shutdown }
  trap("HUP") { @log.info("SIGHUP, graceful shutdown started."); shutdown }
  trap("USR2") { @log.info(status_info) }
end

#shutdown(force = false) ⇒ Object

When called it will set the @shutdown flag indicating to the SCGI::Processor.listen function that all new connections should be set to /busy.html, and all current connections should be “valved” off. Once all the current connections are gone the SCGI::Processor.listen function will exit.

Use the force=true parameter to force an immediate shutdown. This is done by closing the listening socket, so it’s rather violent.



244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
# File 'lib/scgi.rb', line 244

def shutdown(force = false)
  synchronize do
    @shutdown = true
    
    if @threads.length == 0 
      @log.info("Immediate shutdown since nobody is connected.")
      @socket.close
    elsif force
      @log.info("Forcing shutdown.  You may see exceptions.")
      @socket.close
    else
      @log.info("Shutdown requested.  Beginning graceful shutdown with #{@threads.length} connected.")
    end
  end
end

#status_infoObject

Returns a Hash with status information. This is used when dumping data to the logs



227
228
229
230
231
232
233
# File 'lib/scgi.rb', line 227

def status_info
  { 
  :time => Time.now,  :pid => Process.pid, :started => @started,
  :max_conns => @maxconns, :conns => @threads.length, :systimes => Process.times,
  :shutdown => @shutdown, :dead => @dead, :total_conns => @total_conns
  }.inspect
end