Cleaner server shutdowns:

- Remove a workaround for Sanic server not always terminating cleanly
- Terminate worker threads before server stop
- Silent closing of watching WebSocket attempted to open while shutting down
This commit is contained in:
Leo Vasanko 2025-08-17 07:21:03 -06:00
parent 26addb2f7b
commit 33db2c01b4
4 changed files with 12 additions and 15 deletions

View File

@ -119,8 +119,12 @@ async def watch(req, ws):
# Send updates # Send updates
while True: while True:
await ws.send(await q.get()) await ws.send(await q.get())
except RuntimeError as e:
if str(e) == "cannot schedule new futures after shutdown":
return # Server shutting down, drop the WebSocket
raise
finally: finally:
del watching.pubsub[uuid] watching.pubsub.pop(uuid, None) # Remove whether it got added yet or not
def subscribe(uuid, ws): def subscribe(uuid, ws):

View File

@ -43,14 +43,16 @@ async def main_start(app, loop):
app.ctx.threadexec = ThreadPoolExecutor( app.ctx.threadexec = ThreadPoolExecutor(
max_workers=workers, thread_name_prefix="cista-ioworker" max_workers=workers, thread_name_prefix="cista-ioworker"
) )
await watching.start(app, loop) watching.start(app, loop)
@app.after_server_stop # Sanic sometimes fails to execute after_server_stop, so we do it before instead (potentially interrupting handlers)
@app.before_server_stop
async def main_stop(app, loop): async def main_stop(app, loop):
quit.set() quit.set()
await watching.stop(app, loop) watching.stop(app)
app.ctx.threadexec.shutdown() app.ctx.threadexec.shutdown()
logger.debug("Cista worker threads all finished")
@app.on_request @app.on_request

View File

@ -1,6 +1,5 @@
import os import os
import re import re
import signal
from pathlib import Path from pathlib import Path
from sanic import Sanic from sanic import Sanic
@ -12,14 +11,6 @@ def run(*, dev=False):
"""Run Sanic main process that spawns worker processes to serve HTTP requests.""" """Run Sanic main process that spawns worker processes to serve HTTP requests."""
from .app import app from .app import app
# Set up immediate exit on Ctrl+C for faster termination
def signal_handler(signum, frame):
print("\nReceived interrupt signal, exiting immediately...")
os._exit(0)
signal.signal(signal.SIGINT, signal_handler)
signal.signal(signal.SIGTERM, signal_handler)
url, opts = parse_listen(config.config.listen) url, opts = parse_listen(config.config.listen)
# Silence Sanic's warning about running in production rather than debug # Silence Sanic's warning about running in production rather than debug
os.environ["SANIC_IGNORE_PRODUCTION_WARNING"] = "1" os.environ["SANIC_IGNORE_PRODUCTION_WARNING"] = "1"

View File

@ -440,7 +440,7 @@ def watcher_poll(loop):
quit.wait(0.1 + 8 * dur) quit.wait(0.1 + 8 * dur)
async def start(app, loop): def start(app, loop):
global rootpath global rootpath
config.load_config() config.load_config()
rootpath = config.config.path rootpath = config.config.path
@ -454,6 +454,6 @@ async def start(app, loop):
app.ctx.watcher.start() app.ctx.watcher.start()
async def stop(app, loop): def stop(app):
quit.set() quit.set()
app.ctx.watcher.join() app.ctx.watcher.join()