Repository: ipython/ipython

Issue:
Kernel Adapter might append too late
Found out with julia, if I aggressively ask for completion before kernel fully started I got : 

```
KERNEL EXCEPTION
key not found: "inspect_request"

 in eventloop at /Users/bussonniermatthias/.julia/v0.4/IJulia/src/IJulia.jl:123
 in anonymous at task.jl:340
```

Which adapter should have translated.



Discussion:
I'm not sure what you mean by "might append too later", can you clarify?

Adapters predate kernelspecs. @takluyver it probably makes sense to put the msg spec version in the kernelspec, right? Then I wouldn't need to ask the kernel about it at runtime.

Later -> Late. Typo. I suppose some messages go to the kernel before the adapters are in place.

This could probably be also fixed in notebook by preventing execution or completion before the kernel is started. 

I was roughly aiming to keep only static information in the kernel specs, and defer as much as possible to the kernel responding to messages. So the kernelspec is the information needed to start the kernel, and the information needed to work with notebooks when the kernel is not available (e.g. syntax highlighting modes). That's just a rough principle, though, we could add message spec version to it.

If we did, what would we do with kernels that don't specify it? Assume 4, assume 5, assume latest, or fallback to the existing logic for asking the kernel about it when it starts?

I'm not sure, it's just an idea. I don't know a great way to get around the race condition in the way I do it now, but I can probably figure it out.


Resolving patch:
diff --git a/IPython/html/base/zmqhandlers.py b/IPython/html/base/zmqhandlers.py
--- a/IPython/html/base/zmqhandlers.py
+++ b/IPython/html/base/zmqhandlers.py
@@ -1,3 +1,4 @@
+# coding: utf-8
 """Tornado handlers for WebSocket <-> ZMQ sockets."""
 
 # Copyright (c) IPython Development Team.
@@ -11,12 +12,6 @@
 except ImportError:
     from urlparse import urlparse # Py 2
 
-try:
-    from http.cookies import SimpleCookie  # Py 3
-except ImportError:
-    from Cookie import SimpleCookie  # Py 2
-import logging
-
 import tornado
 from tornado import ioloop
 from tornado import web
@@ -24,7 +19,7 @@
 
 from IPython.kernel.zmq.session import Session
 from IPython.utils.jsonutil import date_default, extract_dates
-from IPython.utils.py3compat import PY3, cast_unicode
+from IPython.utils.py3compat import cast_unicode
 
 from .handlers import IPythonHandler
 
@@ -218,14 +213,21 @@ def get(self, *args, **kwargs):
             self.session.session = cast_unicode(self.get_argument('session_id'))
         else:
             self.log.warn("No session ID specified")
-        
-        return super(AuthenticatedZMQStreamHandler, self).get(*args, **kwargs)
+        # FIXME: only do super get on tornado ≥ 4
+        # tornado 3 has no get, will raise 405
+        if tornado.version_info >= (4,):
+            return super(AuthenticatedZMQStreamHandler, self).get(*args, **kwargs)
     
     def initialize(self):
         self.session = Session(config=self.config)
     
-    def open(self, kernel_id):
-        self.kernel_id = cast_unicode(kernel_id, 'ascii')
+    def open(self, *args, **kwargs):
+        if tornado.version_info < (4,):
+            try:
+                self.get(*self.open_args, **self.open_kwargs)
+            except web.HTTPError:
+                self.close()
+                raise
         
         # start the pinging
         if self.ping_interval > 0:
diff --git a/IPython/html/services/kernels/handlers.py b/IPython/html/services/kernels/handlers.py
--- a/IPython/html/services/kernels/handlers.py
+++ b/IPython/html/services/kernels/handlers.py
@@ -5,10 +5,11 @@
 
 import json
 import logging
-from tornado import web
+from tornado import gen, web
+from tornado.concurrent import Future
 
 from IPython.utils.jsonutil import date_default
-from IPython.utils.py3compat import string_types
+from IPython.utils.py3compat import cast_unicode
 from IPython.html.utils import url_path_join, url_escape
 
 from ...base.handlers import IPythonHandler, json_errors
@@ -91,17 +92,26 @@ def create_stream(self):
         km = self.kernel_manager
         meth = getattr(km, 'connect_%s' % self.channel)
         self.zmq_stream = meth(self.kernel_id, identity=self.session.bsession)
-        # Create a kernel_info channel to query the kernel protocol version.
-        # This channel will be closed after the kernel_info reply is received.
-        self.kernel_info_channel = None
-        self.kernel_info_channel = km.connect_shell(self.kernel_id)
-        self.kernel_info_channel.on_recv(self._handle_kernel_info_reply)
-        self._request_kernel_info()
     
-    def _request_kernel_info(self):
+    def request_kernel_info(self):
         """send a request for kernel_info"""
-        self.log.debug("requesting kernel info")
-        self.session.send(self.kernel_info_channel, "kernel_info_request")
+        km = self.kernel_manager
+        kernel = km.get_kernel(self.kernel_id)
+        try:
+            # check for cached value
+            kernel_info = kernel._kernel_info
+        except AttributeError:
+            self.log.debug("Requesting kernel info from %s", self.kernel_id)
+            # Create a kernel_info channel to query the kernel protocol version.
+            # This channel will be closed after the kernel_info reply is received.
+            if self.kernel_info_channel is None:
+                self.kernel_info_channel = km.connect_shell(self.kernel_id)
+            self.kernel_info_channel.on_recv(self._handle_kernel_info_reply)
+            self.session.send(self.kernel_info_channel, "kernel_info_request")
+        else:
+            # use cached value, don't resend request
+            self._finish_kernel_info(kernel_info)
+        return self._kernel_info_future
     
     def _handle_kernel_info_reply(self, msg):
         """process the kernel_info_reply
@@ -113,28 +123,54 @@ def _handle_kernel_info_reply(self, msg):
             msg = self.session.deserialize(msg)
         except:
             self.log.error("Bad kernel_info reply", exc_info=True)
-            self._request_kernel_info()
+            self._kernel_info_future.set_result(None)
             return
         else:
-            if msg['msg_type'] != 'kernel_info_reply' or 'protocol_version' not in msg['content']:
-                self.log.error("Kernel info request failed, assuming current %s", msg['content'])
+            info = msg['content']
+            self.log.debug("Received kernel info: %s", info)
+            if msg['msg_type'] != 'kernel_info_reply' or 'protocol_version' not in info:
+                self.log.error("Kernel info request failed, assuming current %s", info)
             else:
-                protocol_version = msg['content']['protocol_version']
-                if protocol_version != kernel_protocol_version:
-                    self.session.adapt_version = int(protocol_version.split('.')[0])
-                    self.log.info("adapting kernel to %s" % protocol_version)
-        self.kernel_info_channel.close()
+                kernel = self.kernel_manager.get_kernel(self.kernel_id)
+                kernel._kernel_info = info
+            self._finish_kernel_info(info)
+        
+        # close the kernel_info channel, we don't need it anymore
+        if self.kernel_info_channel:
+            self.kernel_info_channel.close()
         self.kernel_info_channel = None
     
+    def _finish_kernel_info(self, info):
+        """Finish handling kernel_info reply
+        
+        Set up protocol adaptation, if needed,
+        and signal that connection can continue.
+        """
+        protocol_version = info.get('protocol_version', kernel_protocol_version)
+        if protocol_version != kernel_protocol_version:
+            self.session.adapt_version = int(protocol_version.split('.')[0])
+            self.log.info("Kernel %s speaks protocol %s", self.kernel_id, protocol_version)
+        self._kernel_info_future.set_result(info)
+    
     def initialize(self):
         super(ZMQChannelHandler, self).initialize()
         self.zmq_stream = None
+        self.kernel_id = None
+        self.kernel_info_channel = None
+        self._kernel_info_future = Future()
+    
+    @gen.coroutine
+    def get(self, kernel_id):
+        self.kernel_id = cast_unicode(kernel_id, 'ascii')
+        yield self.request_kernel_info()
+        super(ZMQChannelHandler, self).get(kernel_id)
     
     def open(self, kernel_id):
-        super(ZMQChannelHandler, self).open(kernel_id)
+        super(ZMQChannelHandler, self).open()
         try:
             self.create_stream()
-        except web.HTTPError:
+        except web.HTTPError as e:
+            self.log.error("Error opening stream: %s", e)
             # WebSockets don't response to traditional error codes so we
             # close the connection.
             if not self.stream.closed():


Test patch: