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
while True:
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:
del watching.pubsub[uuid]
watching.pubsub.pop(uuid, None) # Remove whether it got added yet or not
def subscribe(uuid, ws):

View File

@ -43,14 +43,16 @@ async def main_start(app, loop):
app.ctx.threadexec = ThreadPoolExecutor(
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):
quit.set()
await watching.stop(app, loop)
watching.stop(app)
app.ctx.threadexec.shutdown()
logger.debug("Cista worker threads all finished")
@app.on_request

View File

@ -1,6 +1,5 @@
import os
import re
import signal
from pathlib import Path
from sanic import Sanic
@ -12,14 +11,6 @@ def run(*, dev=False):
"""Run Sanic main process that spawns worker processes to serve HTTP requests."""
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)
# Silence Sanic's warning about running in production rather than debug
os.environ["SANIC_IGNORE_PRODUCTION_WARNING"] = "1"

View File

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