File size: 4,494 Bytes
31e8a9f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
62f312f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
31e8a9f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
62f312f
 
 
 
 
 
 
 
 
 
 
31e8a9f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
"""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()