"""Run before `alembic upgrade`: when there are database changes to apply, ask the backup container for a fresh dump and wait for it, so every upgrade can be rolled back to the moment just before it. The api stops (and Docker retries it) if the backup fails, unless SKIP_PRE_UPGRADE_BACKUP=true. """ import os import sys import time import uuid from pathlib import Path from alembic.config import Config from alembic.runtime.migration import MigrationContext from alembic.script import ScriptDirectory from .db import engine REQUESTS = Path(os.getenv("BACKUP_REQUEST_DIR", "/requests")) TIMEOUT = int(os.getenv("PRE_UPGRADE_BACKUP_TIMEOUT", "900")) def pending() -> tuple[str | None, str]: head = ScriptDirectory.from_config(Config("alembic.ini")).get_current_head() with engine.connect() as conn: current = MigrationContext.configure(conn).get_current_revision() return current, head def main() -> int: current, head = pending() if current is None or current == head: return 0 # new database, or nothing to migrate if os.getenv("SKIP_PRE_UPGRADE_BACKUP", "").lower() == "true": print(f"pre-upgrade backup skipped ({current} -> {head})", flush=True) return 0 if not REQUESTS.is_dir(): print(f"pre-upgrade backup: {REQUESTS} not mounted, cannot ask the backup container", file=sys.stderr, flush=True) return 1 rid = time.strftime("%Y%m%d%H%M%S") + "-" + uuid.uuid4().hex[:6] (REQUESTS / f"{rid}.request").write_text(f"database upgrade {current} -> {head}\n") print(f"database upgrade {current} -> {head}: waiting for the backup container to take a backup", flush=True) done = REQUESTS / f"{rid}.done" deadline = time.time() + TIMEOUT while time.time() < deadline: if done.exists(): answer = done.read_text().strip() # the backup container clears old answers itself if answer == "ok": print("pre-upgrade backup written", flush=True) return 0 print("pre-upgrade backup FAILED; not upgrading. See: docker compose logs backup", file=sys.stderr, flush=True) return 1 time.sleep(2) print("pre-upgrade backup did not answer in time; not upgrading. Is the backup container running?", file=sys.stderr, flush=True) return 1 if __name__ == "__main__": sys.exit(main())