]> git.ipfire.org Git - thirdparty/tornado.git/commitdiff
Integrate websocket periodic pinging code from Jupyter 1642/head
authorThomas Kluyver <thomas@kluyver.me.uk>
Mon, 15 Feb 2016 14:41:27 +0000 (14:41 +0000)
committerThomas Kluyver <thomas@kluyver.me.uk>
Mon, 15 Feb 2016 14:41:27 +0000 (14:41 +0000)
Closes gh-1640

As Ben requested on #1640, I've changed it to work in seconds rather
than milliseconds.

I'm not sure how we'd test something like this. I don't think we have
tests for it in Jupyter.

docs/web.rst
tornado/websocket.py

index dbbc13ce8253092d7e25879588ed185b56a36909..5439b474bfae2b315e1ca3d533cab9e56aa7cb10 100644 (file)
            of `UIModule` or UI methods to be made available to templates.
            May be set to a module, dictionary, or a list of modules
            and/or dicts.  See :ref:`ui-modules` for more details.
+         * ``websocket_ping_interval``: If set to a number, all websockets will
+           be pinged every n seconds. This can help keep the connection alive
+           through certain proxy servers which close idle connections, and it
+           can detect if the websocket has failed without being properly closed.
+         * ``websocket_ping_timeout``: If the ping interval is set, and the
+           server doesn't receive a 'pong' in this many seconds, it will close
+           the websocket. The default is three times the ping interval, with a
+           minimum of 30 seconds. Ignored if the ping interval is not set.
 
          Authentication and security settings:
 
index f5e3dbd7f80a6d939c3323396c89afca28ee410f..bcd74ccac1953a89efc3b94111a27263ca7cf64b 100644 (file)
@@ -31,7 +31,7 @@ import zlib
 from tornado.concurrent import TracebackFuture
 from tornado.escape import utf8, native_str, to_unicode
 from tornado import httpclient, httputil
-from tornado.ioloop import IOLoop
+from tornado.ioloop import IOLoop, PeriodicCallback
 from tornado.iostream import StreamClosedError
 from tornado.log import gen_log, app_log
 from tornado import simple_httpclient
@@ -193,6 +193,29 @@ class WebSocketHandler(tornado.web.RequestHandler):
                     "Sec-WebSocket-Version: 7, 8, 13\r\n\r\n"))
                 self.stream.close()
 
+    ping_callback = None
+    last_ping = 0
+    last_pong = 0
+    stream = None
+
+    @property
+    def ping_interval(self):
+        """The interval for websocket keep-alive pings.
+
+        Set ws_ping_interval = 0 to disable pings.
+        """
+        return self.settings.get('websocket_ping_interval', 0)
+
+    @property
+    def ping_timeout(self):
+        """If no ping is received in this many seconds,
+        close the websocket connection (VPNs, etc. can fail to cleanly close ws connections).
+        Default is max of 3 pings or 30 seconds.
+        """
+        return self.settings.get('websocket_ping_timeout',
+            max(3 * self.ping_interval, 30)
+        )
+
     def write_message(self, message, binary=False):
         """Sends the given message to the client of this Web Socket.
 
@@ -251,6 +274,39 @@ class WebSocketHandler(tornado.web.RequestHandler):
         """
         pass
 
+    def start_pinging(self):
+        """Start sending periodic pings to keep the connection alive"""
+        if self.ping_interval > 0:
+            loop = IOLoop.current()
+            self.last_ping = loop.time()  # Remember time of last ping
+            self.last_pong = self.last_ping
+            self.ping_callback = PeriodicCallback(
+                self.send_ping, self.ping_interval*1000, io_loop=loop,
+            )
+            self.ping_callback.start()
+
+    def send_ping(self):
+        """Send a ping to keep the websocket alive
+
+        Called periodically if the websocket_ping_interval is set and non-zero.
+        """
+        if self.stream.closed() and self.ping_callback is not None:
+            self.ping_callback.stop()
+            return
+
+        # check for timeout on pong.  Make sure that we really have sent a recent ping in
+        # case the machine with both server and client has been suspended since the last ping.
+        now = IOLoop.current().time()
+        since_last_pong = now - self.last_pong
+        since_last_ping = now - self.last_ping
+        if since_last_ping < 2*self.ping_interval and since_last_pong > self.ping_timeout:
+            self.log.warn("WebSocket ping timeout after %i ms.", since_last_pong)
+            self.close()
+            return
+
+        self.ping(b'')
+        self.last_ping = now
+
     def on_message(self, message):
         """Handle incoming messages on the WebSocket
 
@@ -265,8 +321,13 @@ class WebSocketHandler(tornado.web.RequestHandler):
         self.ws_connection.write_ping(data)
 
     def on_pong(self, data):
-        """Invoked when the response to a ping frame is received."""
-        pass
+        """Invoked when the response to a ping frame is received.
+
+        If you override this, be sure to call the parent method, otherwise
+        tornado's regular pinging may decide that the connection has dropped
+        and close the websocket.
+        """
+        self.last_pong = IOLoop.current().time()
 
     def on_close(self):
         """Invoked when the WebSocket is closed.
@@ -592,6 +653,7 @@ class WebSocketProtocol13(WebSocketProtocol):
             "\r\n" % (self._challenge_response(),
                       subprotocol_header, extension_header)))
 
+        self._run_callback(self.handler.start_pinging)
         self._run_callback(self.handler.open, *self.handler.open_args,
                            **self.handler.open_kwargs)
         self._receive_frame()