File size: 6,081 Bytes
88d2f2a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
#!/usr/bin/env bash
# =============================================================================
# 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