Repository: jupyterhub/jupyterhub

Issue:
Support tornado 5
Tests are failing with tornado 5a1. This is likely due to tornado 5 running on asyncio by default. It's my suspicion that the issue lies only in the test suite and not the application, which I've been running locally on tornado master from time to time.


Discussion:

Resolving patch:
diff --git a/jupyterhub/app.py b/jupyterhub/app.py
--- a/jupyterhub/app.py
+++ b/jupyterhub/app.py
@@ -1625,10 +1625,7 @@ def stop(self):
         if not self.io_loop:
             return
         if self.http_server:
-            if self.io_loop._running:
-                self.io_loop.add_callback(self.http_server.stop)
-            else:
-                self.http_server.stop()
+            self.http_server.stop()
         self.io_loop.add_callback(self.io_loop.stop)
 
     @gen.coroutine
diff --git a/jupyterhub/services/auth.py b/jupyterhub/services/auth.py
--- a/jupyterhub/services/auth.py
+++ b/jupyterhub/services/auth.py
@@ -778,7 +778,14 @@ def get_current_user(self):
         except UserNotAllowed as e:
             # cache None, in case get_user is called again while processing the error
             self._hub_auth_user_cache = None
-            raise HTTPError(403, "{kind} {name} is not allowed.".format(**e.model))
+            # Override redirect so if/when tornado @web.authenticated
+            # tries to redirect to login URL, 403 will be raised instead.
+            # This is not the best, but avoids problems that can be caused
+            # when get_current_user is allowed to raise.
+            def raise_on_redirect(*args, **kwargs):
+                raise HTTPError(403, "{kind} {name} is not allowed.".format(**user_model))
+            self.redirect = raise_on_redirect
+            return
         except Exception:
             self._hub_auth_user_cache = None
             raise


Test patch:
diff --git a/jupyterhub/tests/mocking.py b/jupyterhub/tests/mocking.py
--- a/jupyterhub/tests/mocking.py
+++ b/jupyterhub/tests/mocking.py
@@ -1,5 +1,6 @@
 """mock utilities for testing"""
 
+from concurrent.futures import ThreadPoolExecutor
 import os
 import sys
 from tempfile import NamedTemporaryFile
@@ -202,11 +203,11 @@ def _port_default(self):
     @default('authenticator_class')
     def _authenticator_class_default(self):
         return MockPAMAuthenticator
-    
+
     @default('spawner_class')
     def _spawner_class_default(self):
         return MockSpawner
-    
+
     def init_signal(self):
         pass
 
@@ -229,11 +230,25 @@ def initialize(self, argv=None):
 
     def stop(self):
         super().stop()
-        IOLoop().run_sync(self.cleanup)
+
+        # run cleanup in a background thread
+        # to avoid multiple eventloops in the same thread errors from asyncio
+
+        def cleanup():
+            loop = IOLoop.current()
+            loop.run_sync(self.cleanup)
+            loop.close()
+
+        pool = ThreadPoolExecutor(1)
+        f = pool.submit(cleanup)
+        # wait for cleanup to finish
+        f.result()
+        pool.shutdown()
+
         # ignore the call that will fire in atexit
         self.cleanup = lambda : None
         self.db_file.close()
-    
+
     @gen.coroutine
     def login_user(self, name):
         """Login a user by name, returning her cookies."""