Spaces:
Running
Running
| # ============================================================================= | |
| # PolyglotAlpha β container entrypoint (W23, Hugging Face Spaces) | |
| # ============================================================================= | |
| # Boots three processes inside one HF Spaces container: | |
| # 1. uvicorn (FastAPI, 127.0.0.1:8000) β the backend | |
| # 2. next start (Next.js prod, 127.0.0.1:3001) β the UI | |
| # 3. nginx (0.0.0.0:7860) β the only port HF exposes externally | |
| # | |
| # A few practical concerns the script handles: | |
| # * If the Space has persistent storage mounted at `/data`, copy the | |
| # seeded SQLite DB there on first boot and re-point DATABASE_URL. | |
| # * Wait for backend health before firing the pre-seed events so the | |
| # leaderboard / events list aren't empty on the reviewer's first visit. | |
| # * Trap SIGTERM/SIGINT so HF's graceful shutdown actually kills children | |
| # (the default `wait` keeps the parent alive but leaves zombies if we | |
| # don't propagate the signal). | |
| # ----------------------------------------------------------------------------- | |
| set -euo pipefail | |
| log() { | |
| printf '[%s entrypoint] %s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$*" >&2 | |
| } | |
| # ----------------------------------------------------------------------------- | |
| # Persistent storage handling β HF Spaces "small persistent storage" tier | |
| # mounts a writable /data volume that survives sleeps + redeploys. Without | |
| # it, the SQLite DB lives in the container FS and resets on every redeploy | |
| # (acceptable for a pure demo; cold starts still pre-seed below). | |
| # ----------------------------------------------------------------------------- | |
| if [[ -d /data && -w /data ]]; then | |
| log "persistent /data detected β relocating SQLite DB" | |
| if [[ ! -f /data/polyglot_alpha.db ]]; then | |
| if [[ -f /app/polyglot_alpha.db ]]; then | |
| cp /app/polyglot_alpha.db /data/polyglot_alpha.db | |
| log "seeded /data/polyglot_alpha.db from container image" | |
| fi | |
| fi | |
| export DATABASE_URL="sqlite:////data/polyglot_alpha.db" | |
| log "DATABASE_URL=${DATABASE_URL}" | |
| else | |
| log "no /data volume β using in-image SQLite (will reset on redeploy)" | |
| fi | |
| # ----------------------------------------------------------------------------- | |
| # 1. Backend | |
| # ----------------------------------------------------------------------------- | |
| cd /app | |
| log "starting uvicorn on 127.0.0.1:8000" | |
| python -m uvicorn polyglot_alpha.api.main:app \ | |
| --host 127.0.0.1 \ | |
| --port 8000 \ | |
| --log-level info \ | |
| --no-access-log & | |
| BACKEND_PID=$! | |
| # ----------------------------------------------------------------------------- | |
| # 2. Frontend | |
| # ----------------------------------------------------------------------------- | |
| log "starting Next.js on 127.0.0.1:3001" | |
| ( | |
| cd /app/ui | |
| exec npx --no-install next start -H 127.0.0.1 -p 3001 | |
| ) & | |
| FRONTEND_PID=$! | |
| # ----------------------------------------------------------------------------- | |
| # Wait for backend health before firing the pre-seed lifecycle. We bound the | |
| # wait at ~60 s so a wedged backend still surfaces (nginx will return 502 on | |
| # /health and HF Spaces will show the failure clearly). | |
| # ----------------------------------------------------------------------------- | |
| wait_for_backend() { | |
| local attempts=0 | |
| while (( attempts < 60 )); do | |
| if curl -fsS --max-time 2 http://127.0.0.1:8000/health > /dev/null 2>&1; then | |
| log "backend healthy after ${attempts}s" | |
| return 0 | |
| fi | |
| sleep 1 | |
| attempts=$((attempts + 1)) | |
| done | |
| log "backend never reached /health β proceeding anyway" | |
| return 1 | |
| } | |
| # ----------------------------------------------------------------------------- | |
| # Pre-seed events β fire 3 mock lifecycles so the leaderboard / events / | |
| # history pages have something to show the first reviewer. We background | |
| # this so nginx can come up immediately. The `|| true` ensures any single | |
| # failure doesn't take down the whole entrypoint. | |
| # ----------------------------------------------------------------------------- | |
| preseed_events() { | |
| wait_for_backend || return 0 | |
| log "pre-seeding 3 mock events" | |
| local i | |
| for i in 1 2 3; do | |
| curl -fsS -X POST http://127.0.0.1:8000/trigger/event \ | |
| -H "Content-Type: application/json" \ | |
| -d '{"mode":"mock"}' > /dev/null 2>&1 || \ | |
| log "pre-seed event ${i} failed (continuing)" | |
| sleep 4 | |
| done | |
| log "pre-seed complete" | |
| } | |
| # Run pre-seed in a `disown`-ed subshell so its natural exit (after 3 | |
| # events β 14s) does NOT trigger the `wait -n` teardown below. Only the | |
| # 3 critical processes (backend, frontend, nginx) should bring down | |
| # the container when they die. | |
| ( preseed_events ) & | |
| disown $! | |
| # ----------------------------------------------------------------------------- | |
| # 3. nginx β run in foreground as PID 1's last child so its exit takes down | |
| # the container if proxy config is broken (fail-fast). | |
| # ----------------------------------------------------------------------------- | |
| log "starting nginx on 0.0.0.0:7860" | |
| nginx -g 'daemon off;' & | |
| NGINX_PID=$! | |
| # ----------------------------------------------------------------------------- | |
| # Signal handling β propagate SIGTERM/SIGINT to all children. HF Spaces | |
| # sends SIGTERM with a 30s grace before SIGKILL. | |
| # ----------------------------------------------------------------------------- | |
| shutdown() { | |
| log "received shutdown signal β forwarding to children" | |
| kill -TERM "$BACKEND_PID" "$FRONTEND_PID" "$NGINX_PID" 2>/dev/null || true | |
| wait "$BACKEND_PID" "$FRONTEND_PID" "$NGINX_PID" 2>/dev/null || true | |
| log "all children exited β bye" | |
| exit 0 | |
| } | |
| trap shutdown SIGTERM SIGINT | |
| # Wait on the 3 critical PIDs explicitly β `wait -n` without args | |
| # would catch ANY background job (including the pre-seed loop), so we | |
| # enumerate just the long-running services. If backend/frontend/nginx | |
| # dies, abort the container so failure surfaces in HF logs. | |
| wait -n "$BACKEND_PID" "$FRONTEND_PID" "$NGINX_PID" | |
| log "a critical child process exited β tearing down the rest" | |
| shutdown | |