<div dir="ltr">Hello,<br><br>I've been trying to implement a simple notification system via long polling using Twisted 14.0.0. On the client side, a web page makes Ajax calls to an "update" resource, which will deliver the latest status message known to the application. On the server side, a thread feeds a queue with status events and another one polls this queue and sends the latest value to any pending request, trying to keep the request open for 30 seconds, or until a new status event arrives, whichever happens sooner (this is the long polling).<br><br>Occasionally, one of responses will just hang. "request.finish()" is called properly (I've even checked this with "notifyFinish()"), the response headers are sent, but the response body isn't (according to Wireshark), thereby leaving the browser hanging (since it thinks a valid response has started to come through).<br><br>I have noticed this behaviour with Twisted 13 and 14, but not with earlier versions. This is also an intermittent problem. I have been able to reproduce it a number of times, but not always, and it doesn't necessarily happen after the same number of requests. It seems to fail more easily on machines with one CPU core, or when the execution is limited to one core, with only one request at a time (for some reason, having concurrent requests from multiple browsers or tabs, or being able to use multiple CPUs on the server seems to make the problem harder to reproduce).<br><br>I'm pasting at the end of this message a simple example where the status events are coming from a counter running in a separate thread. When limiting the server process to one CPU, the counter visible on the webpage rarely seems to go much above 2000. I've only tried under Linux. I've tried this on an actual PC (limiting to one core using "taskset -c 0 python test_server.py"), a VM running in VirtualBox (set to 1 CPU) and even a Raspberry Pi. The Raspberry Pi is where the problem seems to be the easiest to reproduce, but this also happens with the actual PC and virtual machine. This was also only tried on a LAN (considering that, for the purpose of this test case, the notifications are rather frequent). I've tried with the default chunked encoding, or by setting the content-length explicitly, but it didn't make a difference.<br><br>Is there a way to force to the response to be flushed, after a call to "request.finish()"? Any other suggestions to fix this would also be appreciated.<br><br>Thank you,<br><br>Bruno.<br><br><br><br><br>Only 2 files are required for this test case: counter.html, test_server.py.<br>I've run this in a virtual environment, with Python 2.7 (in which I've installed Twisted with `pip install twisted==14.0.0`).<br>On a multicore machine, to limit the execution to one core, call the server with taskset: "taskset -c 0 python test_server.py".<br>Once the server is running, point a browser (tried with Chrome and Firefox) to "<a href="http://the.ip.address.example:8880/">http://the.ip.address.example:8880/</a>", you should see a counter increasing (and eventually stopping, which illustrates the problem). The issue seems to happen more often when only 1 client (or tab) is connected to the server.<br><br><br>- counter.html:<br><br><html><br><head><br><script type="application/javascript" src="//<a href="http://code.jquery.com/jquery-1.11.0.min.js">code.jquery.com/jquery-1.11.0.min.js</a>"></script><br></head><br><body><br><div id="counter" style="font-size: 100pt; font-weight: bold;"></div><br><script type="application/javascript"><br>// <![CDATA[<br>$(document).ready(function() {<br>    var eTag = null;<br>    var normalRefreshDelay = 50;<br>    var refreshDelay = normalRefreshDelay;<br>    var maxRefreshDelay = 5000;<br>    function refresh() {<br>        var headers = {};<br>        if (eTag) {<br>            headers['If-None-Match'] = eTag;<br>        }<br>        $.ajax({<br>            url: "/update",<br>            headers: headers,<br>            dataType: "json",<br>            cache: false,<br>            success: function(data, status, xhr) {<br>                try {<br>                    if (data[1] != null) { $("#counter").text("Counter: "+data[1]); }<br>                    eTag = xhr.getResponseHeader("ETag") || null;<br>                    refreshDelay = normalRefreshDelay;<br>                } catch (e) {<br>                    refreshDelay = Math.min(refreshDelay * 2, maxRefreshDelay);<br>                }<br>            },<br>            error: function(xhr, text, error) {<br>                refreshDelay = Math.min(refreshDelay * 2, maxRefreshDelay);<br>            },<br>            complete: function(xhr, textStatus) {<br>                setTimeout(refresh, refreshDelay);<br>            }<br>        });<br>    }<br>    refresh();<br>});<br>// ]]><br></script><br></body><br></html><br><br><br><br>- test_server.py<br><br><br># -*- coding: utf-8 -*-<br><br>import threading<br>import time<br>import Queue<br>import uuid<br>import socket<br>import json<br><br>from twisted.web.server import Site<br>from twisted.web.resource import Resource<br>from twisted.web.server import NOT_DONE_YET<br>from twisted.internet import reactor<br>from twisted.web.static import File<br><br>q = Queue.Queue()<br><br>def fake_counter():<br>    i = 1<br>    while True:<br>        q.put({ 1: str(i) })<br>        time.sleep(0.15)<br>        i += 1<br><br>producer_thread = threading.Thread(target=fake_counter)<br>producer_thread.daemon = True<br>producer_thread.start()<br><br><br>class CounterUpdateResource(Resource):<br>    def __init__(self, msg_queue):<br>        Resource.__init__(self)<br>        self.msg_queue = msg_queue<br>        self.msg = {}<br>        self.etag = None<br>        self.pending_requests = []<br>        <br>        t = threading.Thread(target=self._poll_messages)<br>        t.daemon = True<br>        t.start()<br>        <br>    def _poll_messages(self):<br>        while True:<br>            try:<br>                self.msg = self.msg_queue.get(True, 30)<br>                self.etag = '"%s"' % (uuid.uuid4(),)<br>                print "-> Got new message: %s" % (self.msg,)<br>            except Queue.Empty:<br>                pass<br>            json_str = json.dumps(self.msg)<br>            json_len = len(json_str)<br>            while True:<br>                try:<br>                    request = self.pending_requests.pop()<br>                    self._actual_render(request, json_str, json_len)<br>                except IndexError:<br>                    break<br>            <br>    def _actual_render(self, request, json_msg, json_len):<br>        try:<br>            request.setETag(self.etag)<br>            request.setHeader("Content-Type", "application/json")<br>            request.setHeader("Content-Length", json_len)<br>            request.write(json_msg)<br>            request.finish()<br>        except:<br>            pass<br><br>    def render_GET(self, request):<br>        ifNoneMatchHeader = request.getHeader("If-None-Match")<br>        if ifNoneMatchHeader is None or ifNoneMatchHeader != self.etag:<br>            if self.etag:<br>                request.setETag(self.etag)<br>            request.setHeader("Content-Type", "application/json")<br>            data = json.dumps(self.msg)<br>            request.setHeader("Content-Length", len(data))<br>            return data<br>        else:<br>            self.pending_requests.append(request)<br>            return NOT_DONE_YET<br><br>root = Resource()<br>root.putChild("", File("counter.html"))<br>root.putChild("update", CounterUpdateResource(q))<br><br>factory = Site(root)<br>reactor.listenTCP(8880, factory)<br>reactor.run()<br><br><br><br><br><br><br>- Sample Wireshark output:<br><br><br>Here is a sample output using Wireshark:<br><br>GET /update?_=1410440411047 HTTP/1.1<br>Host: <a href="http://192.168.0.8:8880">192.168.0.8:8880</a><br>Connection: keep-alive<br>Cache-Control: max-age=0<br>Accept: application/json, text/javascript, */*; q=0.01<br>X-Requested-With: XMLHttpRequest<br>User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/37.0.2062.103 Safari/537.36<br>If-None-Match: "84c25895-de1e-4b50-954b-438be9d8d9b7"<br>Referer: <a href="http://192.168.0.8:8880/">http://192.168.0.8:8880/</a><br>Accept-Encoding: gzip,deflate,sdch<br>Accept-Language: en-GB,en;q=0.8,en-US;q=0.6<br><br>HTTP/1.1 200 OK<br>Date: Thu, 11 Sep 2014 13:06:23 GMT<br>Content-Length: 12<br>ETag: "7736231b-ee3a-4a9f-a93e-ced0994df098"<br>Content-Type: application/json<br>Server: TwistedWeb/14.0.0<br><br>{"1": "371"}<br><br>GET /update?_=1410440411048 HTTP/1.1<br>Host: <a href="http://192.168.0.8:8880">192.168.0.8:8880</a><br>Connection: keep-alive<br>Cache-Control: max-age=0<br>Accept: application/json, text/javascript, */*; q=0.01<br>X-Requested-With: XMLHttpRequest<br>User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/37.0.2062.103 Safari/537.36<br>If-None-Match: "7736231b-ee3a-4a9f-a93e-ced0994df098"<br>Referer: <a href="http://192.168.0.8:8880/">http://192.168.0.8:8880/</a><br>Accept-Encoding: gzip,deflate,sdch<br>Accept-Language: en-GB,en;q=0.8,en-US;q=0.6<br><br>HTTP/1.1 200 OK<br>Date: Thu, 11 Sep 2014 13:06:23 GMT<br>Content-Length: 12<br>ETag: "ddd10400-3530-4cb4-a9cd-0b123032d8af"<br>Content-Type: application/json<br>Server: TwistedWeb/14.0.0<br><br><br><br>(I've inserted a coupe of line returns after "{"1": "371"}" for readability.)<br>Here, nothing is sent on the wire after the headers of the last response (the "\r\n\r\n" are present).<br><br><br></div>