chatbot1-12 Exercise 3: The Shutdown Sequence ================================================ ex3.py (save in the project root): # Exercise 3: graceful shutdown. Run the whole service in-process, talk to it, stop it, and look at what shutdown did. import io, json, logging, os, sqlite3, tempfile from fastapi.testclient import TestClient from bot.app import build_service from bot.crypto import generate_key from bot.observability import setup_logging from bot.settings import load_settings d = tempfile.mkdtemp() stream = io.StringIO() logger = setup_logging(logging.StreamHandler(stream), name="exercise3") s = load_settings({"CHATBOT_DATA_DIR": d, "CHATBOT_PROVIDER": "echo", "CHATBOT_CONTENT_KEYS": generate_key(), "CHATBOT_PERSONA_FILE": os.path.join(d, "none")}) service = build_service(s, logger=logger) with TestClient(service.app, base_url="http://127.0.0.1:8000", headers={"Origin": "http://127.0.0.1:8000"}) as client: client.get("/") print("ready while running:", client.get("/readyz").json()) print("after the with-block (shutdown ran):") try: service.bot.store.db.execute("SELECT 1") except sqlite3.ProgrammingError as e: print(" database closed:", e) for line in stream.getvalue().splitlines(): entry = json.loads(line) if entry["message"] in ("started", "stopping", "stopped"): print(" log:", entry["message"]) Run with: py -W ignore ex3.py (the flag hides a Starlette deprecation warning about its test client) Output: ready while running: {'ready': True, 'channels': []} after the with-block (shutdown ran): database closed: Cannot operate on a closed database. log: started log: stopping log: stopped What this does NOT prove: it leaves the with-block, which runs the same lifespan code that runs on shutdown, but nobody sent a SIGTERM, no reply was in flight, and no Telegram, Discord or Slack adapter was configured. On a server, test the real thing: start a long streamed reply, run systemctl stop chatbot, and check that the reply finished (or was cut cleanly), and that the journal shows "stopping" and "stopped". WHY THIS WORKS AS AN ANSWER --------------------------- The lifespan function logs "started", then on exit logs "stopping", cancels the background tasks, drains the adapters (with a time limit), closes their HTTP clients and finally closes the database. Touching the database afterwards raises "Cannot operate on a closed database", which proves the last step ran; the log lines prove the order.