Cleanup everything on API shutdown

via app.stop()
This commit is contained in:
Min RK
2022-05-05 12:10:20 +02:00
parent 585b47051f
commit bf2e322c22
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

@@ -3234,9 +3234,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:
@@ -3253,7 +3259,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):
@@ -3261,7 +3266,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

@@ -188,6 +188,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

@@ -325,26 +325,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

@@ -2095,14 +2095,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