Spaces:
Sleeping
Sleeping
| """Idempotent Alembic upgrade on app startup (Postgres / HF Spaces). | |
| Gunicorn runs multiple workers — only the leader runs migrations (fcntl lock). | |
| SQLite and pytest are skipped; disable with RUN_ALEMBIC_ON_STARTUP=false. | |
| """ | |
| from __future__ import annotations | |
| import logging | |
| import os | |
| import sys | |
| from typing import Any | |
| logger = logging.getLogger(__name__) | |
| def _env_flag(name: str, default: bool = True) -> bool: | |
| val = os.environ.get(name) | |
| if val is None: | |
| return default | |
| return val.lower() not in ("0", "false", "no", "off") | |
| def _running_under_pytest() -> bool: | |
| return ( | |
| "PYTEST_CURRENT_TEST" in os.environ | |
| or os.environ.get("ENV", "").lower() == "test" | |
| or "pytest" in sys.modules | |
| ) | |
| def _is_postgres_url(url: str | None) -> bool: | |
| if not url: | |
| return False | |
| return url.startswith("postgresql://") or url.startswith("postgres://") | |
| def _acquire_migrate_leader() -> bool: | |
| """Only one worker runs Alembic (Gunicorn multi-worker).""" | |
| lock_path = os.environ.get( | |
| "ALEMBIC_LOCK_PATH", | |
| os.path.join(os.environ.get("TMPDIR", "/tmp"), "grantforge_alembic.lock"), | |
| ) | |
| try: | |
| import fcntl | |
| lock_file = open(lock_path, "w") | |
| fcntl.flock(lock_file.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB) | |
| lock_file.write(str(os.getpid())) | |
| lock_file.flush() | |
| # Keep FD open for process lifetime so the lock is held. | |
| globals()["_alembic_lock_fd"] = lock_file | |
| return True | |
| except OSError: | |
| logger.info("[db_migrate] Inny worker już uruchomił Alembic — pomijam.") | |
| return False | |
| except ImportError: | |
| return True | |
| def _backend_root() -> str: | |
| return os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) | |
| def run_alembic_upgrade_head() -> dict[str, Any]: | |
| """Run ``alembic upgrade head`` when DATABASE_URL is Postgres. | |
| Returns a small status dict (no credentials). Idempotent when already at head. | |
| """ | |
| status: dict[str, Any] = {"ran": False, "skipped": True, "reason": None} | |
| if not _env_flag("RUN_ALEMBIC_ON_STARTUP", default=True): | |
| status["reason"] = "RUN_ALEMBIC_ON_STARTUP=false" | |
| logger.info("[db_migrate] Pomijam Alembic (%s).", status["reason"]) | |
| return status | |
| db_url = os.environ.get("DATABASE_URL", "") | |
| if not _is_postgres_url(db_url): | |
| status["reason"] = "not_postgres" | |
| logger.info("[db_migrate] DATABASE_URL nie jest Postgres — pomijam Alembic.") | |
| return status | |
| if _running_under_pytest(): | |
| status["reason"] = "pytest" | |
| logger.info("[db_migrate] Środowisko testowe — pomijam Alembic.") | |
| return status | |
| if not _acquire_migrate_leader(): | |
| status["reason"] = "not_leader" | |
| return status | |
| backend_root = _backend_root() | |
| ini_path = os.path.join(backend_root, "alembic.ini") | |
| if not os.path.isfile(ini_path): | |
| status["reason"] = "alembic_ini_missing" | |
| status["skipped"] = False | |
| logger.error("[db_migrate] Brak pliku %s — nie mogę uruchomić migracji.", ini_path) | |
| return status | |
| try: | |
| from alembic import command | |
| from alembic.config import Config | |
| cfg = Config(ini_path) | |
| cfg.set_main_option("script_location", os.path.join(backend_root, "alembic")) | |
| # env.py reads DATABASE_URL from the environment; do not log the URL. | |
| logger.info("[db_migrate] Uruchamiam alembic upgrade head (Postgres)...") | |
| command.upgrade(cfg, "head") | |
| status["ran"] = True | |
| status["skipped"] = False | |
| status["reason"] = "ok" | |
| logger.info("[db_migrate] Alembic upgrade head zakończony pomyślnie.") | |
| except Exception as e: | |
| status["ran"] = False | |
| status["skipped"] = False | |
| status["reason"] = "error" | |
| status["error"] = type(e).__name__ | |
| logger.error( | |
| "[db_migrate] Alembic upgrade head nieudany: %s: %s. " | |
| "Serwer kontynuuje z create_all — sprawdź logi / schema.", | |
| type(e).__name__, | |
| e, | |
| ) | |
| return status | |