Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions deploy/CLOUDRUN.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ files — the Dockerfile builds it into the image). Cloud Run scales it to
| Port | `8000` |
| Auth | public (`--allow-unauthenticated`) |
| Request timeout | `600s` (`--timeout 600` — see note) |
| CPU allocation | always-on (`--no-cpu-throttling` — see async-job note below) |

> **Why 600s:** a live illustration is a two-step generation (an establish-shot
> still, then an image-to-video clip chained from it) that runs for minutes.
Expand All @@ -22,6 +23,15 @@ files — the Dockerfile builds it into the image). Cloud Run scales it to
> so a synchronous `POST /cases/render` outlives the real generation path.
> (In offline mode render is near-instant.)

> **Why `--no-cpu-throttling`:** by default Cloud Run only allocates CPU to an
> instance while it has a request in flight — the moment a response is sent,
> CPU is throttled to near-zero. `POST /cases/render/jobs` (async submit +
> poll — see below) returns its `202` immediately and keeps rendering in a
> **background thread** after that response has already gone out. Without
> `--no-cpu-throttling` that thread would barely progress between polls. This
> only affects billing while a job is actually in flight — Cloud Run still
> scales to zero (and bills nothing) when idle.

## Prerequisites (one-time)

```bash
Expand Down Expand Up @@ -52,6 +62,33 @@ curl -s "$URL/scenarios" | head -c 200 # committed sample scenarios
curl -s -o /dev/null -w '%{http_code}\n' "$URL/" # 200 (React SPA index)
```

## Async job submission (submit + poll)

`POST /cases/render/jobs` / `GET /cases/render/jobs/{job_id}` exist alongside
the synchronous `POST /cases/render` for the same reason `--timeout 600`
does: edge proxies in front of Cloud Run — Firebase Hosting's rewrite proxy in
particular — cap a single request at **~60s**, well under a real two-step
generation's several minutes. The async pair splits that into a fast submit
(`202` + a job id) and a cheap poll (`GET /cases/render/jobs/{job_id}` →
`queued` / `running` / `done` / `failed`), so no single HTTP request needs to
stay open for the full render. See `src/claimscene/jobs.py` for the job-store
design (status objects live in the same B2/fake storage backend the rest of
the app already uses, under `jobs/<job_id>/status.json` — not a separate
database).

**Honest limitation:** the background worker runs **in-process, on the Cloud
Run instance that accepted the submit** — there is no external queue and no
separate worker fleet. A client that keeps polling keeps that instance warm
(a request in flight resets Cloud Run's idle-scale-down clock), so in
practice the job completes. But if that specific instance is scaled down
mid-job, the in-flight generation is lost with no automatic retry — the
stored status simply stops advancing. This is a deliberate, acceptable
tradeoff for a demo, not a production job queue; a production version would
hand the work to a durable queue (e.g. Cloud Tasks) consumed by a Cloud Run
**Job** (not a request-serving instance), so the work outlives any one
instance. `--no-cpu-throttling` (above) is required even for this demo
version — without it, the worker thread barely progresses between polls.

## Deploy — LIVE cutover (secrets via Secret Manager)

The live path extracts scenes with the VLM ladder, renders real illustrations
Expand Down
6 changes: 6 additions & 0 deletions deploy/deploy-cloudrun.sh
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,11 @@ fi
# --timeout 600 from the start: a live illustration (still + image-to-video
# clip) runs for minutes; the default 300s edge deadline would 504 the
# synchronous POST /cases/render while the case completes server-side.
# --no-cpu-throttling: POST /cases/render/jobs runs the actual two-step
# generation in a background thread AFTER the request that submitted it has
# already returned (see src/claimscene/jobs.py) — by default Cloud Run only
# allocates CPU while a request is in flight, which would starve that thread
# between polls. See deploy/CLOUDRUN.md for the full async-job note.
gcloud run deploy "${SERVICE}" \
--image "${IMAGE}" \
--region "${REGION}" \
Expand All @@ -134,6 +139,7 @@ gcloud run deploy "${SERVICE}" \
--port 8000 \
--timeout 600 \
--cpu 1 --memory 1Gi \
--no-cpu-throttling \
--min-instances 0 --max-instances 4 \
--set-env-vars "${ENV_VARS}" \
${SET_SECRETS_ARGS[@]+"${SET_SECRETS_ARGS[@]}"} \
Expand Down
5 changes: 5 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ authors = [{ name = "upgradedev" }]
dependencies = [
"pydantic>=2.6",
"pillow>=10.2",
# Firebase ID-token verification (claimscene.auth) — RS256 JWT decode +
# Google's x509 signing certs. No Firebase Admin SDK, no service-account
# secret.
"pyjwt>=2.8",
"cryptography>=42",
]

[project.optional-dependencies]
Expand Down
5 changes: 5 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,8 @@
# extra — see pyproject [live]. Keep this list pip-audit --strict clean.
pydantic>=2.6
pillow>=10.2
# Firebase ID-token verification (claimscene.auth) — RS256 JWT decode +
# Google's x509 signing certs. No Firebase Admin SDK, no service-account
# secret.
pyjwt>=2.8
cryptography>=42
Loading
Loading