Repository: bokeh/bokeh

Issue:
DocumentLifeCycleHandler should catch exception and clean up callbacks
The DocumentLifeCycleHandler allows adding on_session_destroyed callbacks to a Document, which works pretty well. However if a single callback fails for whatever reason other callbacks won't be called. Additionally the references to the callbacks themselves may be prevent references from being collected, therefore the handler should delete the callbacks once it is done with them.


Discussion:

Resolving patch:
diff --git a/bokeh/application/handlers/document_lifecycle.py b/bokeh/application/handlers/document_lifecycle.py
--- a/bokeh/application/handlers/document_lifecycle.py
+++ b/bokeh/application/handlers/document_lifecycle.py
@@ -62,8 +62,22 @@ def _on_session_destroyed(session_context):
     '''
     Calls any on_session_destroyed callbacks defined on the Document
     '''
-    for callback in session_context._document._session_destroyed_callbacks:
-        callback(session_context)
+    callbacks = session_context._document.session_destroyed_callbacks
+    session_context._document.session_destroyed_callbacks = set()
+    for callback in callbacks:
+        try:
+            callback(session_context)
+        except Exception as e:
+            log.warn('DocumentLifeCycleHandler on_session_destroyed '
+                     'callback %s failed with following error: %s'
+                     % (callback, e))
+    if callbacks:
+        # If any session callbacks were defined garbage collect after deleting all references
+        del callback
+        del callbacks
+
+        import gc
+        gc.collect()
 
 #-----------------------------------------------------------------------------
 # Code
diff --git a/bokeh/document/document.py b/bokeh/document/document.py
--- a/bokeh/document/document.py
+++ b/bokeh/document/document.py
@@ -101,7 +101,7 @@ def __init__(self, **kwargs):
         self._all_models_by_name = MultiValuedDict()
         self._all_former_model_ids = set()
         self._callbacks = {}
-        self._session_destroyed_callbacks = {}
+        self._session_destroyed_callbacks = set()
         self._session_callbacks = set()
         self._session_context = None
         self._modules = []
@@ -132,6 +132,17 @@ def session_callbacks(self):
         '''
         return list(self._session_callbacks)
 
+    @property
+    def session_destroyed_callbacks(self):
+        ''' A list of all the on_session_destroyed callbacks on this document.
+
+        '''
+        return self._session_destroyed_callbacks
+
+    @session_destroyed_callbacks.setter
+    def session_destroyed_callbacks(self, callbacks):
+        self._session_destroyed_callbacks = callbacks
+
     @property
     def session_context(self):
         ''' The ``SessionContext`` for this document.
@@ -664,7 +675,7 @@ def on_session_destroyed(self, *callbacks):
         '''
         for callback in callbacks:
             _check_callback(callback, ('session_context',))
-            self._session_destroyed_callbacks[callback] = callback
+            self._session_destroyed_callbacks.add(callback)
 
     def remove_next_tick_callback(self, callback_obj):
         ''' Remove a callback added earlier with ``add_next_tick_callback``.


Test patch:
diff --git a/bokeh/application/handlers/tests/test_document_lifecycle.py b/bokeh/application/handlers/tests/test_document_lifecycle.py
--- a/bokeh/application/handlers/tests/test_document_lifecycle.py
+++ b/bokeh/application/handlers/tests/test_document_lifecycle.py
@@ -75,6 +75,7 @@ def destroy(session_context):
         session_context = MockSessionContext(doc)
         handler.on_session_destroyed(session_context)
         assert session_context.status == 'Destroyed'
+        assert session_context._document.session_destroyed_callbacks == set()
 
     def test_document_on_session_destroyed_calls_multiple(self):
         doc = Document()