Package grassyknoll :: Package concurrent :: Module WhiskeyServer
[hide private]

Source Code for Module grassyknoll.concurrent.WhiskeyServer

  1  """a U{http://wsgi.org/wsgi wsgi} server based on L{wsgiref} and L{ThreadPool} 
  2   
  3  Whoever wrote SocketServer (and its various spawn, including BaseHTTPServer & 
  4  wsgiref) should be shot, and their corpse killed until it is dead. The author 
  5  of the WSGI standard itself should then be buried alive with the 
  6  aforementioned corpse. In the author's opinion, this would be a fate too kind 
  7  for both of them. 
  8  """ 
  9   
 10  import time 
 11  import signal 
 12  import logging 
 13  from wsgiref import handlers 
 14  import wsgiref.simple_server 
 15   
 16  from grassyknoll.lib.meta import AutoLogger 
 17   
 18  import ThreadPool 
 19  import MailBox 
 20  import Worker 
 21  import Message 
 22  import Wrappers 
 23  from errors import * 
 24   
 25  __version__="0.3" 
 26   
 27  default_too_busy_content=""" 
 28  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> 
 29  <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> 
 30   
 31  <head> 
 32  <title>Service Unavailable</title> 
 33  </head> 
 34   
 35  <body> 
 36  <h1>Service Unavailable</h1> 
 37      <p>The server is too busy to handle your request. Please try again in a few minutes.</p> 
 38  </body> 
 39  </html> 
 40  """ 
 41   
42 -class WhiskeyRequestHandler(wsgiref.simple_server.WSGIRequestHandler):
43 44 server_version = "GrassyKnoll-WhiskeyServer/" + __version__ 45 46 logger=AutoLogger() 47
48 - def log_error(self, format, *args):
49 self.logger.warn("[%s] "+format, self.address_string(), *args)
50
51 - def log_message(self, format, *args):
52 """Log an arbitrary message. 53 54 Prepends currentThread() to standard L{BaseHTTPServer.BaseHTTPRequestHandler.log_message} 55 """ 56 self.logger.info("[%s] "+format, self.address_string(), *args)
57 58 ## monkeypatch wsgiref to use logging. Horrid!
59 -class WhiskeyServerHandler(wsgiref.simple_server.ServerHandler):
60 logger=AutoLogger()
61 - def handle_error(self):
62 self.logger.error("Internal Error", exc_info=True) 63 if not self.headers_sent: 64 self.result = self.error_output(self.environ, self.start_response) 65 self.finish_response()
66 wsgiref.simple_server.ServerHandler=WhiskeyServerHandler 67
68 -class WhiskeyServer(wsgiref.simple_server.WSGIServer):
69 """a threadpool-based wsgi server. 70 71 @ivar threadpool: a threadpool to handle requests 72 @type threadpool: L{ThreadPool.ThreadPool} 73 """ 74 logger=AutoLogger() 75
76 - def __init__(self, host_port, num_workers, qsize, 77 handler_class=WhiskeyRequestHandler, 78 too_busy_content=default_too_busy_content):
79 """ 80 C{host}, C{port} and C{handler_class} are interpreted by L{WSGIServer}. 81 82 C{num_workers} and C{qsize} are interpreted by L{ThreadPool.ThreadPool} 83 84 @arg too_busy_content: text/html content to send to browsers when the 85 server is overloaded. This content is sent with a Status: 503 header 86 and the connection is closed. 87 @type too_busy_content: str 88 """ 89 if not isinstance(too_busy_content, str): 90 raise TypeError("too_busy_content must be str, " 91 "not %r"%type(too_busy_content).__name__) 92 93 self.__too_busy_response=self.__make_too_busy_response(too_busy_content) 94 95 self.threadpool=ThreadPool.ThreadPool( 96 Wrappers.NullContextManager.factory(func=self.process_request_thread), 97 inbox=MailBox.ThreadMailBox(name="whiskeyserver", qsize=qsize), 98 name="whiskeyserver", 99 num_workers=num_workers, 100 unhandled="keeprunning") 101 102 wsgiref.simple_server.WSGIServer.__init__(self, host_port, handler_class) 103 self._stop=False
104
105 - def serve_forever(self):
106 """Handle one request at a time until shutdown().""" 107 self.threadpool.start() 108 while not self._stop: 109 self.handle_request() 110 111 self.logger.info("Server shutting down") 112 self.server_close() 113 self.threadpool.stop() 114 self.logger.info("Shutdown complete")
115
116 - def shutdown(self):
117 """stop the server""" 118 # XXX it'd be nice to break serve_forever out of the blocking accept() 119 # in handle_request, but that requires another thread. See 120 # http://mail.python.org/pipermail/python-list/2003-February/188485.html 121 self._stop=True
122
123 - def install_signal_handler(self):
124 """install a handler for SIGTERM, providing clean shutdown""" 125 def signal_handler(foo, bar): 126 self.logger.debug("Got SIGTERM, shutting down") 127 self.shutdown()
128 129 return signal.signal(signal.SIGTERM, signal_handler)
130 131 @Wrappers.methodMessenger()
132 - def process_request_thread(self, request, client_address):
133 """process a single request, in a L{Worker.Worker} thread. 134 135 Derrived from L{SocketServer.ThreadingMixIn} 136 137 @arg request_client_address: a handle to a client request. a 2-tuple 138 of (socket, client_address). 139 @type request_client_address: tuple 140 """ 141 try: 142 self.finish_request(request, client_address) 143 finally: 144 self.close_request(request)
145
146 - def process_request(self, request, client_address):
147 """process incoming client requests. 148 149 This method delivers the requests to the server's 150 L{ThreadPool.ThreadPool} to be serviced. 151 152 If the threadpool raises L{BoxFullError}, call L{too_busy} and allow 153 the connection to be closed immediately. 154 """ 155 mesg = Message.Message(Wrappers.FunctionCall(request, client_address)) 156 try: 157 self.threadpool.inbox.sendNow(mesg) 158 except BoxFullError: 159 self.too_busy(request, client_address)
160
161 - def too_busy(self, request, client_address):
162 """Called when the threadpool won't accept new requests. 163 164 This method sends too_busy_content to client with a Status: 503 header. 165 166 It also calls L{log_too_busy}. 167 """ 168 self.log_too_busy(client_address[0]) 169 request.sendall(self.__too_busy_response) 170 self.close_request(request)
171
172 - def log_too_busy(self, remote_ip):
173 """Called to log L{too_busy} 174 175 Subclassers may override, but should run as quickly as possible, as 176 your server is on fire. 177 178 @arg remote_ip: the IP address of the remote connection 179 @type remote_ip: string 180 """ 181 182 self.logger.warn("[%s] Too Busy", remote_ip)
183
184 - def __make_too_busy_response(self, msg):
185 status = '503' 186 protocol='HTTP/1.0' 187 188 headers=[('Content-Type', 'text/html'), 189 ('Date', handlers.format_date_time(time.time())), 190 ('Status', status), 191 ('Connection', 'close')] 192 193 return self.__format_simple_response(protocol, status, headers, msg)
194
195 - def __format_simple_response(self, protocol, status, headers, content=""):
196 """Format a simple response for sending back to the client.""" 197 status = str(status) 198 buf=['%s: %s\r\n'%(protocol, status)] 199 buf.extend("%s %s\r\n"%(h) for h in headers) 200 buf.append("Content-Length: %s\r\n" % len(content)) 201 buf.append("\r\n") 202 if content: 203 buf.append(content) 204 return "".join(buf)
205