Backport PR #3886: Cleanup everything on API shutdown

`app.stop` triggers full cleanup and stopping of the event loop

closes  3881

Signed-off-by: Min RK <benjaminrk@gmail.com>
This commit is contained in:
Simon Li
2022-05-05 20:16:43 +01:00
committed by Min RK
parent d40627d397
commit 3879a96b67
5 changed files with 38 additions and 21 deletions

View File

@@ -47,9 +47,8 @@ class ShutdownAPIHandler(APIHandler):
self.set_status(202) self.set_status(202)
self.finish(json.dumps({"message": "Shutting down Hub"})) self.finish(json.dumps({"message": "Shutting down Hub"}))
# stop the eventloop, which will trigger cleanup # instruct the app to stop, which will trigger cleanup
loop = IOLoop.current() app.stop()
loop.add_callback(loop.stop)
class RootAPIHandler(APIHandler): class RootAPIHandler(APIHandler):

View File

@@ -3241,9 +3241,15 @@ class JupyterHub(Application):
loop.make_current() loop.make_current()
loop.run_sync(self.cleanup) loop.run_sync(self.cleanup)
async def shutdown_cancel_tasks(self, sig): async def shutdown_cancel_tasks(self, sig=None):
"""Cancel all other tasks of the event loop and initiate cleanup""" """Cancel all other tasks of the event loop and initiate cleanup"""
self.log.critical("Received signal %s, initiating shutdown...", sig.name) if sig is None:
self.log.critical("Initiating shutdown...")
else:
self.log.critical("Received signal %s, initiating shutdown...", sig.name)
await self.cleanup()
tasks = [t for t in asyncio_all_tasks() if t is not asyncio_current_task()] tasks = [t for t in asyncio_all_tasks() if t is not asyncio_current_task()]
if tasks: if tasks:
@@ -3260,7 +3266,6 @@ class JupyterHub(Application):
tasks = [t for t in asyncio_all_tasks()] tasks = [t for t in asyncio_all_tasks()]
for t in tasks: for t in tasks:
self.log.debug("Task status: %s", t) self.log.debug("Task status: %s", t)
await self.cleanup()
asyncio.get_event_loop().stop() asyncio.get_event_loop().stop()
def stop(self): def stop(self):
@@ -3268,7 +3273,7 @@ class JupyterHub(Application):
return return
if self.http_server: if self.http_server:
self.http_server.stop() self.http_server.stop()
self.io_loop.add_callback(self.io_loop.stop) self.io_loop.add_callback(self.shutdown_cancel_tasks)
async def start_show_config(self): async def start_show_config(self):
"""Async wrapper around base start_show_config method""" """Async wrapper around base start_show_config method"""

View File

@@ -191,6 +191,8 @@ def cleanup_after(request, io_loop):
if not MockHub.initialized(): if not MockHub.initialized():
return return
app = MockHub.instance() app = MockHub.instance()
if app.db_file.closed:
return
for uid, user in list(app.users.items()): for uid, user in list(app.users.items()):
for name, spawner in list(user.spawners.items()): for name, spawner in list(user.spawners.items()):
if spawner.active: if spawner.active:

View File

@@ -333,26 +333,28 @@ class MockHub(JupyterHub):
roles.assign_default_roles(self.db, entity=user) roles.assign_default_roles(self.db, entity=user)
self.db.commit() self.db.commit()
def stop(self): _stop_called = False
super().stop()
def stop(self):
if self._stop_called:
return
self._stop_called = True
# run cleanup in a background thread # run cleanup in a background thread
# to avoid multiple eventloops in the same thread errors from asyncio # to avoid multiple eventloops in the same thread errors from asyncio
def cleanup(): def cleanup():
asyncio.set_event_loop(asyncio.new_event_loop()) loop = asyncio.new_event_loop()
loop = IOLoop.current() loop.run_until_complete(self.cleanup())
loop.run_sync(self.cleanup)
loop.close() loop.close()
pool = ThreadPoolExecutor(1) with ThreadPoolExecutor(1) as pool:
f = pool.submit(cleanup) f = pool.submit(cleanup)
# wait for cleanup to finish # wait for cleanup to finish
f.result() f.result()
pool.shutdown()
# ignore the call that will fire in atexit # prevent redundant atexit from running
self.cleanup = lambda: None self._atexit_ran = True
super().stop()
self.db_file.close() self.db_file.close()
async def login_user(self, name): async def login_user(self, name):

View File

@@ -2104,14 +2104,23 @@ def test_shutdown(app):
) )
return r return r
real_stop = loop.stop real_stop = loop.asyncio_loop.stop
def stop(): def stop():
stop.called = True stop.called = True
loop.call_later(1, real_stop) loop.call_later(1, real_stop)
with mock.patch.object(loop, 'stop', stop): real_cleanup = app.cleanup
def cleanup():
cleanup.called = True
return real_cleanup()
app.cleanup = cleanup
with mock.patch.object(loop.asyncio_loop, 'stop', stop):
r = loop.run_sync(shutdown, timeout=5) r = loop.run_sync(shutdown, timeout=5)
r.raise_for_status() r.raise_for_status()
reply = r.json() reply = r.json()
assert cleanup.called
assert stop.called assert stop.called