mirror of
https://github.com/jupyterhub/jupyterhub.git
synced 2025-10-15 05:53:00 +00:00
Cleanup everything on API shutdown
via app.stop()
This commit is contained in:
@@ -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):
|
||||||
|
@@ -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"""
|
||||||
|
@@ -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:
|
||||||
|
@@ -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):
|
||||||
|
@@ -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
|
||||||
|
Reference in New Issue
Block a user