On my app server which is built upon twisted+web2 I have seen that at times some of worker threads will get blocked and won't get released ever. Eventually my app server runs out of worker threads and starts throwing message "503 Service not available"

On more debugging I have found that I can easily recreate this situation by doing a incomplete HTTP Post with data less then content length specified in POST header. After this incomplete HTTP Post with lesser bytes of data if I close the client socket or kill client process or reboot client machine, in all these three cases it results in a hanged thread on my app server. This hanged threads remains on app server for ever till kill app server process, no time out of any kind.

Further debugging into web2 code I found that stream.BufferedStream.readExactly doesn't come out of stream.BufferedStrem._readUntil even if I close client side socket. After putting some debug statements I found that self.stream.read() in stream.BufferedStream._readUntil doesn't return None as a signal for End Of File even after client has closed the socket connection. Since it doesn't get None it will never return with whatever data it has received so far from client.

To reproduce this issue, I have written a minimal twisted-web2 server and a client that does a HTTP Post to it with incomplete data. After Post it closes the client socket and sleeps for sometime. Finally it prints the stack of all process threads, which clearly shows one blocked twisted worker thread. In the same program if you change it to do complete HTTP Post, in the thread dump there is no blocked twisted worker thread.

Here is the stack traceback of blocked twisted worker thread.

  File "C:\Python24\lib\threading.py", line 442, in __bootstrap
    self.run()

  File "C:\Python24\lib\threading.py", line 422, in run
    self.__target(*self.__args, **self.__kwargs)

  File "/home/lotus/Desktop/pywork3.3/lm/foreign-common/src/twisted/python/threadpool.py", line 161, in _worker

  File "/home/lotus/Desktop/pywork3.3/lm/foreign-common/src/twisted/python/context.py", line 59, in callWithContext

  File "/home/lotus/Desktop/pywork3.3/lm/foreign-common/src/twisted/python/context.py", line 37, in callWithContext

  File "/home/lotus/Desktop/pywork3.3/lm/foreign-common/src/twisted/web2/wsgi.py", line 190, in run

  File "C:\work\twisted-head\twisted_server3.py", line 18, in dummy_app
    out_content = environ['wsgi.input'].read(int(environ['CONTENT_LENGTH']))

  File "/home/lotus/Desktop/pywork3.3/lm/foreign-common/src/twisted/web2/wsgi.py", line 74, in read

  File "/home/lotus/Desktop/pywork3.3/lm/foreign-common/src/twisted/internet/threads.py", line 81, in blockingCallFromThread

  File "C:\Python24\lib\Queue.py", line 119, in get
    self.not_empty.wait()

  File "C:\Python24\lib\threading.py", line 203, in wait
    waiter.acquire()


To run this program twisted, twisted.web2, zope and threadframe modules are required.



import sys, time, socket
from threading import Thread
import threadframe, traceback
from twisted.web2 import server, channel, static, wsgi, resource
from twisted.internet import reactor

class TwistedWebServerThread(Thread):

    def __init__(self, app):
        Thread.__init__(self, name="Twisted")
        factory = channel.HTTPFactory(server.Site(wsgi.WSGIResource(app)))
        reactor.listenTCP(8080, factory, interface="0.0.0.0")

    def run(self):
        reactor.run(installSignalHandlers=False)

def dummy_app(environ, start_response):
    out_content = environ['wsgi.input'].read(int(environ['CONTENT_LENGTH']))
    start_response('200 OK', [('Content-type', 'text/plain')])
    return [out_content]

#data intentionally less then content length
D = "POST /cgi-bin/minapi.py HTTP/1.0\r\n"+ \
 "Host: 127.0.0.1:8080\r\n"+\
 "Content-Type: text/xml\r\n"+\
 "Content-Length: 152\r\n"+\
 "\r\n"+\
 "<?xml version='1.0'?>\n"+\
 "<methodCall>\n"+\
 "<methodName>getStateName</methodName>\n" +\
 "<params>\n" +\
 "<param>\n" +\
 "<value><int>41</int></value>\n" #+\
# "</param>\n" +\
# "</params>\n" +\
# "</methodCall>\n"

#D = open("data.txt3", "rb").read()

def half_post():
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.connect(("127.0.0.1", 8080))
    s.send(D)
    s.close()

def write_server_threads():
    frames = threadframe.threadframe()
    for frame in frames:
        print '-' * 72
        for linestr in traceback.format_stack(frame):
            print linestr

if __name__ == "__main__":
    s = TwistedWebServerThread(dummy_app)
    s.start()
    time.sleep(0.2)
    half_post()
    time.sleep(5)
    write_server_threads()