Repository: ipython/ipython

Issue:
Changing kernels quickly leaks fds and crashes the notebook
If you open a notebook and then rapidly change between different kernels a couple of things happen:
- The main server process leaks fds and eventually runs out (quickly on OS X).
- The notebook IU will alter on websocket connection failures.
- The terminal will log the following:

```
[E 15:11:06.765 tornado.application] Uncaught exception in /api/kernels/4920106a-3f6c-42ac-96fb-6d6520372225/shell
    Traceback (most recent call last):
      File "/Users/bgranger/anaconda/lib/python2.7/site-packages/tornado/websocket.py", line 322, in wrapper
        return callback(*args, **kwargs)
      File "/Users/bgranger/github/ipython/ipython/IPython/html/services/kernels/handlers.py", line 149, in on_message
        self.session.send(self.zmq_stream, msg)
      File "/Users/bgranger/github/ipython/ipython/IPython/kernel/zmq/session.py", line 658, in send
        stream.send_multipart(to_send, copy=copy)
    AttributeError: 'NoneType' object has no attribute 'send_multipart'
```

My guess is that the issue is caused by changing kernels before the previous kernel was fully running.



Discussion:
I have verified that this happens regardless of which kernels you switch between, so it doesn't appear to be special to the R or bash kernel.

Part of the reason I am observing this is that there are still times where Ipython processes take 10-30s to start on my very recent Mac.

@ellisonbg I am seeing the crash too developing new kernels based on the kernel wrapper. Usually, the second attempt to change the kernel via the UI works.


Resolving patch:
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
@@ -84,6 +84,9 @@ def post(self, kernel_id, action):
 
 class ZMQChannelHandler(AuthenticatedZMQStreamHandler):
     
+    def __repr__(self):
+        return "%s(%s)" % (self.__class__.__name__, getattr(self, 'kernel_id', 'uninitialized'))
+    
     def create_stream(self):
         km = self.kernel_manager
         meth = getattr(km, 'connect_%s' % self.channel)
@@ -145,6 +148,12 @@ def on_first_message(self, msg):
             self.zmq_stream.on_recv(self._on_zmq_reply)
 
     def on_message(self, msg):
+        if self.zmq_stream is None:
+            return
+        elif self.zmq_stream.closed():
+            self.log.info("%s closed, closing websocket.", self)
+            self.close()
+            return
         msg = json.loads(msg)
         self.session.send(self.zmq_stream, msg)
 


Test patch:
diff --git a/IPython/html/tests/services/kernel.js b/IPython/html/tests/services/kernel.js
--- a/IPython/html/tests/services/kernel.js
+++ b/IPython/html/tests/services/kernel.js
@@ -1,12 +1,12 @@
 
 //
-// Miscellaneous javascript tests
+// Kernel tests
 //
 casper.notebook_test(function () {
     this.evaluate(function () {
         IPython.notebook.kernel.kernel_info(
             function(msg){
-                    IPython._kernel_info_response = msg;
+                IPython._kernel_info_response = msg;
             })
     });
 
@@ -24,5 +24,41 @@ casper.notebook_test(function () {
         this.test.assertTrue( kernel_info_response.msg_type === 'kernel_info_reply', 'Kernel info request return kernel_info_reply');
         this.test.assertTrue( kernel_info_response.content !== undefined, 'Kernel_info_reply is not undefined');
     });
-
+    
+    this.thenEvaluate(function () {
+        var kernel = IPython.notebook.session.kernel;
+        IPython._channels = [
+            kernel.shell_channel,
+            kernel.iopub_channel,
+            kernel.stdin_channel
+        ];
+        kernel.kill();
+    });
+    
+    this.waitFor(function () {
+        return this.evaluate(function(){
+            for (var i=0; i < IPython._channels.length; i++) {
+                var ws = IPython._channels[i];
+                if (ws.readyState !== ws.CLOSED) {
+                    return false;
+                }
+            }
+            return true;
+        });
+    });
+    
+    this.then(function () {
+        var states = this.evaluate(function() {
+            var states = [];
+            for (var i = 0; i < IPython._channels.length; i++) {
+                states.push(IPython._channels[i].readyState);
+            }
+            return states;
+        });
+        
+        for (var i = 0; i < states.length; i++) {
+            this.test.assertEquals(states[i], WebSocket.CLOSED,
+                "Kernel.kill closes websockets[" + i + "]");
+        }
+    });
 });
diff --git a/IPython/html/tests/services/session.js b/IPython/html/tests/services/session.js
new file mode 100644
--- /dev/null
+++ b/IPython/html/tests/services/session.js
@@ -0,0 +1,43 @@
+
+//
+// Tests for the Session object
+//
+
+casper.notebook_test(function () {
+    this.evaluate(function () {
+        var kernel = IPython.notebook.session.kernel;
+        IPython._channels = [
+            kernel.shell_channel,
+            kernel.iopub_channel,
+            kernel.stdin_channel
+        ];
+        IPython.notebook.session.delete();
+    });
+    
+    this.waitFor(function () {
+        return this.evaluate(function(){
+            for (var i=0; i < IPython._channels.length; i++) {
+                var ws = IPython._channels[i];
+                if (ws.readyState !== ws.CLOSED) {
+                    return false;
+                }
+            }
+            return true;
+        });
+    });
+
+    this.then(function () {
+        var states = this.evaluate(function() {
+            var states = [];
+            for (var i = 0; i < IPython._channels.length; i++) {
+                states.push(IPython._channels[i].readyState);
+            }
+            return states;
+        });
+        
+        for (var i = 0; i < states.length; i++) {
+            this.test.assertEquals(states[i], WebSocket.CLOSED,
+                "Session.delete closes websockets[" + i + "]");
+        }
+    });
+});