vera-api / app.py
prathvi9607's picture
Fix wake-up race: commit dispatch mark before publishing
62f312f verified
Raw History Blame Contribute Delete
4.49 kB
"""VERA API + worker launcher for a free Hugging Face Gradio Space on ZeroGPU hardware.
Why this shape: Docker Spaces and CPU-basic Gradio Spaces require a PRO plan, while a Gradio
Space on ZeroGPU hardware is free. ZeroGPU stops any app that never reports startup, and that
report is normally sent by ``gradio.Blocks.launch``. This launcher instead serves the existing
FastAPI app with uvicorn and sends the report itself (``spaces.zero.startup``). All inference
runs on CPU with onnxruntime; the GPU is never requested, so the ZeroGPU quota is not used.
One container owns: loopback Redis (Celery broker), Alembic migrations, one solo Celery worker
(which downloads the models from ``HF_MODEL_REPO`` and runs the outbox dispatcher), and the API
on port 7860. If Redis or the worker exits, the process exits so the Space restarts.
Required Space secrets: DATABASE_URL, HF_MODEL_REPO, HF_TOKEN (read), SECRET_KEY, CORS_ORIGINS.
"""
from __future__ import annotations
import os
import subprocess
import sys
import threading
import time
from pathlib import Path
import spaces # must be imported before any CUDA-related package
ROOT = Path(__file__).resolve().parent
BACKEND = ROOT / "backend"
MODEL_DIR = Path(os.environ.get("MODEL_DIR", "/home/user/models"))
REDIS_URL = "redis://127.0.0.1:6379/0"
for name in ("DATABASE_URL", "HF_MODEL_REPO"):
if not os.environ.get(name):
sys.exit(f"Set {name} in the Space secrets")
os.environ.update(
ENVIRONMENT="production",
MODEL_DIR=str(MODEL_DIR),
REDIS_URL=REDIS_URL,
CELERY_BROKER_URL=REDIS_URL,
CELERY_RESULT_BACKEND="",
)
MODEL_DIR.mkdir(parents=True, exist_ok=True)
sys.path.insert(0, str(BACKEND))
os.chdir(BACKEND)
@spaces.GPU
def _zerogpu_placeholder() -> str:
"""ZeroGPU only reports startup when at least one GPU function exists. Never called."""
return "unused"
def _log(message: str) -> None:
print(f"[vera-launcher] {message}", flush=True)
def start_redis() -> subprocess.Popen[bytes]:
Path("/tmp/redis").mkdir(exist_ok=True)
proc = subprocess.Popen(
[
"redis-server",
"--bind",
"127.0.0.1",
"--protected-mode",
"yes",
"--port",
"6379",
"--dir",
"/tmp/redis",
"--save",
"",
"--appendonly",
"no",
]
)
for _ in range(30):
ping = subprocess.run(["redis-cli", "-h", "127.0.0.1", "ping"], capture_output=True)
if ping.stdout.strip() == b"PONG":
_log("redis ready")
return proc
if proc.poll() is not None:
break
time.sleep(1)
sys.exit("redis-server failed to start")
def run_migrations() -> None:
subprocess.run([sys.executable, "-m", "alembic", "upgrade", "head"], check=True)
_log("database migrations applied")
def start_worker() -> subprocess.Popen[bytes]:
return subprocess.Popen(
[
sys.executable,
"-m",
"celery",
"-A",
"app.worker.celery_app",
"worker",
"--pool=solo",
"--concurrency=1",
"--loglevel=info",
]
)
def supervise(children: dict[str, subprocess.Popen[bytes]]) -> None:
"""Exit the whole Space if a required child dies, so the platform restarts it."""
while True:
for name, proc in children.items():
if proc.poll() is not None:
_log(f"{name} exited with code {proc.returncode}; stopping the Space")
for other in children.values():
if other.poll() is None:
other.terminate()
os._exit(1)
time.sleep(5)
def main() -> None:
import uvicorn
redis_proc = start_redis()
run_migrations()
worker = start_worker()
threading.Thread(
target=supervise, args=({"redis": redis_proc, "worker": worker},), daemon=True
).start()
from app.main import app
@app.get("/", include_in_schema=False)
def root() -> dict[str, str]:
return {"service": "VERA review analysis API", "health": "/health", "ready": "/ready"}
zero = getattr(spaces, "zero", None)
if zero is not None and hasattr(zero, "startup"):
zero.startup()
_log("ZeroGPU startup reported")
uvicorn.run(app, host="0.0.0.0", port=7860, proxy_headers=False, access_log=False)
if __name__ == "__main__":
main()