60 lines
2.3 KiB
Python
60 lines
2.3 KiB
Python
"""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())
|