debugging websocket connections

- add debug statement at the very beginning of a web socket request
- add debug statement in open, indicating that the connection has been accepted
- add timeout, so failed or slow kernel_info doesn't cause the request to never get a response
- don't send kernel_info_request before authenticating the request

The last one required some icky coroutine shenanigans,
because of our subclass structure, but it should work fine.
This commit is contained in:
Min RK 2014-10-28 18:49:15 -07:00
parent b26706ba69
commit e5b135a905
2 changed files with 43 additions and 8 deletions

View File

@ -13,9 +13,7 @@ except ImportError:
from urlparse import urlparse # Py 2 from urlparse import urlparse # Py 2
import tornado import tornado
from tornado import ioloop from tornado import gen, ioloop, web, websocket
from tornado import web
from tornado import websocket
from IPython.kernel.zmq.session import Session from IPython.kernel.zmq.session import Session
from IPython.utils.jsonutil import date_default, extract_dates from IPython.utils.jsonutil import date_default, extract_dates
@ -197,7 +195,12 @@ class AuthenticatedZMQStreamHandler(ZMQStreamHandler, IPythonHandler):
""" """
pass pass
def get(self, *args, **kwargs): def pre_get(self):
"""Run before finishing the GET request
Extend this method to add logic that should fire before
the websocket finishes completing.
"""
# Check to see that origin matches host directly, including ports # Check to see that origin matches host directly, including ports
# Tornado 4 already does CORS checking # Tornado 4 already does CORS checking
if tornado.version_info[0] < 4: if tornado.version_info[0] < 4:
@ -213,15 +216,22 @@ class AuthenticatedZMQStreamHandler(ZMQStreamHandler, IPythonHandler):
self.session.session = cast_unicode(self.get_argument('session_id')) self.session.session = cast_unicode(self.get_argument('session_id'))
else: else:
self.log.warn("No session ID specified") self.log.warn("No session ID specified")
@gen.coroutine
def get(self, *args, **kwargs):
# pre_get can be a coroutine in subclasses
yield gen.maybe_future(self.pre_get())
# FIXME: only do super get on tornado ≥ 4 # FIXME: only do super get on tornado ≥ 4
# tornado 3 has no get, will raise 405 # tornado 3 has no get, will raise 405
if tornado.version_info >= (4,): if tornado.version_info >= (4,):
return super(AuthenticatedZMQStreamHandler, self).get(*args, **kwargs) super(AuthenticatedZMQStreamHandler, self).get(*args, **kwargs)
def initialize(self): def initialize(self):
self.log.debug("Initializing websocket connection %s", self.request.path)
self.session = Session(config=self.config) self.session = Session(config=self.config)
def open(self, *args, **kwargs): def open(self, *args, **kwargs):
self.log.debug("Opening websocket %s", self.request.path)
if tornado.version_info < (4,): if tornado.version_info < (4,):
try: try:
self.get(*self.open_args, **self.open_kwargs) self.get(*self.open_args, **self.open_kwargs)

View File

@ -7,6 +7,7 @@ import json
import logging import logging
from tornado import gen, web from tornado import gen, web
from tornado.concurrent import Future from tornado.concurrent import Future
from tornado.ioloop import IOLoop
from IPython.utils.jsonutil import date_default from IPython.utils.jsonutil import date_default
from IPython.utils.py3compat import cast_unicode from IPython.utils.py3compat import cast_unicode
@ -85,6 +86,10 @@ class KernelActionHandler(IPythonHandler):
class ZMQChannelHandler(AuthenticatedZMQStreamHandler): class ZMQChannelHandler(AuthenticatedZMQStreamHandler):
@property
def kernel_info_timeout(self):
return self.settings.get('kernel_info_timeout', 10)
def __repr__(self): def __repr__(self):
return "%s(%s)" % (self.__class__.__name__, getattr(self, 'kernel_id', 'uninitialized')) return "%s(%s)" % (self.__class__.__name__, getattr(self, 'kernel_id', 'uninitialized'))
@ -150,6 +155,7 @@ class ZMQChannelHandler(AuthenticatedZMQStreamHandler):
if protocol_version != kernel_protocol_version: if protocol_version != kernel_protocol_version:
self.session.adapt_version = int(protocol_version.split('.')[0]) self.session.adapt_version = int(protocol_version.split('.')[0])
self.log.info("Kernel %s speaks protocol %s", self.kernel_id, protocol_version) self.log.info("Kernel %s speaks protocol %s", self.kernel_id, protocol_version)
if not self._kernel_info_future.done():
self._kernel_info_future.set_result(info) self._kernel_info_future.set_result(info)
def initialize(self): def initialize(self):
@ -159,11 +165,30 @@ class ZMQChannelHandler(AuthenticatedZMQStreamHandler):
self.kernel_info_channel = None self.kernel_info_channel = None
self._kernel_info_future = Future() self._kernel_info_future = Future()
@gen.coroutine
def pre_get(self):
# authenticate first
super(ZMQChannelHandler, self).pre_get()
# then request kernel info, waiting up to a certain time before giving up.
# We don't want to wait forever, because browsers don't take it well when
# servers never respond to websocket connection requests.
future = self.request_kernel_info()
def give_up():
"""Don't wait forever for the kernel to reply"""
if future.done():
return
self.log.warn("Timeout waiting for kernel_info reply from %s", self.kernel_id)
future.set_result(None)
loop = IOLoop.current()
loop.add_timeout(loop.time() + self.kernel_info_timeout, give_up)
# actually wait for it
yield future
@gen.coroutine @gen.coroutine
def get(self, kernel_id): def get(self, kernel_id):
self.kernel_id = cast_unicode(kernel_id, 'ascii') self.kernel_id = cast_unicode(kernel_id, 'ascii')
yield self.request_kernel_info() yield super(ZMQChannelHandler, self).get(kernel_id=kernel_id)
super(ZMQChannelHandler, self).get(kernel_id)
def open(self, kernel_id): def open(self, kernel_id):
super(ZMQChannelHandler, self).open() super(ZMQChannelHandler, self).open()