Repository: bokeh/bokeh

Issue:
Bokeh Prod issues
Under low stress bokeh server seems to have some protocol handling errors.  Several users reported errors from the console (also nginx) like the following:
### console

```
Bokeh: Unhandled ERROR reply to FA407C214347428090A609FFD122E432: AttributeError("'NoneType' object has no attribute 'value'",)
```
### nginx

```
ERROR:bokeh.server.protocol.server_handler:error handling message Message 'PATCH-DOC' (revision 1): AttributeError("'NoneType' object has no attribute 'value'",)
DEBUG:bokeh.server.protocol.server_handler:  message header {u'msgid': u'FA407C214347428090A609FFD122E432', u'msgtype': u'PATCH-DOC'} content {u'references': [], u'events': [{u'new': 2.63894, u'kind': u'ModelChanged', u'model': {u'type':
u'Slider', u'id': u'd29d43c1-447a-4c4d-84ad-1d11daff73ce'}, u'attr': u'value'}]}
```

Bokeh Prod issues
Under low stress bokeh server seems to have some protocol handling errors.  Several users reported errors from the console (also nginx) like the following:
### console

```
Bokeh: Unhandled ERROR reply to FA407C214347428090A609FFD122E432: AttributeError("'NoneType' object has no attribute 'value'",)
```
### nginx

```
ERROR:bokeh.server.protocol.server_handler:error handling message Message 'PATCH-DOC' (revision 1): AttributeError("'NoneType' object has no attribute 'value'",)
DEBUG:bokeh.server.protocol.server_handler:  message header {u'msgid': u'FA407C214347428090A609FFD122E432', u'msgtype': u'PATCH-DOC'} content {u'references': [], u'events': [{u'new': 2.63894, u'kind': u'ModelChanged', u'model': {u'type':
u'Slider', u'id': u'd29d43c1-447a-4c4d-84ad-1d11daff73ce'}, u'attr': u'value'}]}
```



Discussion:
@havocp so it seems at a glance that looking up the model id in the `PATCH-DOC` returns `None` I'm not sure offhand what might cause that. Crosstalk/contamination between sessions, the ID is getting changed somewhere somehow, the model gets deleted?

I was seeing this too. Does it happen if we have only one server instance? One possibility is that we load the html with id's in it from one server, then connect websocket to a different server. But I don't know if that's it.
Our current config has no session affinity.

could also be related to disconnection (we have no keep-alive right now, and I'm not sure what nginx does by default)

I thought of session affinity, so ben restarted with one server and it still seems to be an issue. 

I now have it running with 15 -- perhaps swinging in the other direction could help ?

I'm still able to reproduce the bug

heh -- I realized that running n+1 server doesn't make a whole lot of sense if we don't change the port

@havocp so it seems at a glance that looking up the model id in the `PATCH-DOC` returns `None` I'm not sure offhand what might cause that. Crosstalk/contamination between sessions, the ID is getting changed somewhere somehow, the model gets deleted?

I was seeing this too. Does it happen if we have only one server instance? One possibility is that we load the html with id's in it from one server, then connect websocket to a different server. But I don't know if that's it.
Our current config has no session affinity.

could also be related to disconnection (we have no keep-alive right now, and I'm not sure what nginx does by default)

I thought of session affinity, so ben restarted with one server and it still seems to be an issue. 

I now have it running with 15 -- perhaps swinging in the other direction could help ?

I'm still able to reproduce the bug

heh -- I realized that running n+1 server doesn't make a whole lot of sense if we don't change the port


Resolving patch:
diff --git a/bokeh/application/handlers/script.py b/bokeh/application/handlers/script.py
--- a/bokeh/application/handlers/script.py
+++ b/bokeh/application/handlers/script.py
@@ -75,14 +75,21 @@ def modify_document(self, doc):
         if self.failed:
             return
         from types import ModuleType
-        self._module_name = 'bk_script_' + str(uuid.uuid4()).replace('-', '')
-        self._module = ModuleType(self._module_name)
-        self._module.__dict__['__file__'] = abspath(self._path)
+        module_name = 'bk_script_' + str(uuid.uuid4()).replace('-', '')
+        module = ModuleType(module_name)
+        module.__dict__['__file__'] = abspath(self._path)
+        # This is to prevent the module from being gc'd before the
+        # document is.  A symptom of a gc'd module is that its
+        # globals become None.
+        if not hasattr(doc, '_ScriptHandler__modules'):
+            setattr(doc, '_ScriptHandler__modules', [])
+        doc.__modules.append(module)
+
         old_doc = curdoc()
         set_curdoc(doc)
         old_io = self._monkeypatch_io()
         try:
-            exec(self._code, self._module.__dict__)
+            exec(self._code, module.__dict__)
             newdoc = curdoc()
             # script is supposed to edit the doc not replace it
             if newdoc is not doc:
diff --git a/bokeh/server/protocol/server_handler.py b/bokeh/server/protocol/server_handler.py
--- a/bokeh/server/protocol/server_handler.py
+++ b/bokeh/server/protocol/server_handler.py
@@ -38,7 +38,7 @@ def handle(self, message, connection):
             work = yield handler(message, connection)
         except Exception as e:
             log.error("error handling message %r: %r", message, e)
-            log.debug("  message header %r content %r", message.header, message.content)
+            log.debug("  message header %r content %r", message.header, message.content, exc_info=1)
             work = connection.error(message, repr(e))
         raise gen.Return(work)
 


Test patch: