diff --git a/.env.example b/.env.example index 6eb2537..2845ff9 100644 --- a/.env.example +++ b/.env.example @@ -3,5 +3,6 @@ REDIS_URL=redis://redis:6379/0 REQUEST_TIMEOUT_SECONDS=1.5 ROUTER_STRATEGY=load_aware CACHE_ENABLED=true +GATEWAY_CONTROLS_ENABLED=true SIMULATE_DELAY_MS=0 FAIL_RATE=0 diff --git a/README.md b/README.md index fc63f6f..59d7ed3 100644 --- a/README.md +++ b/README.md @@ -62,6 +62,13 @@ Deployment notes are in `docs/deployment.md`. There is also a Render Blueprint in `render.yaml` for a hosted demo with one public gateway, two private workers, and a managed cache. +## Hosted demo + +The hosted demo is Render-ready but does not have a checked-in public URL yet. The Blueprint +keeps workers private and disables public worker failure controls. + +Deployment and validation steps are in `docs/hosted-demo.md`. + ## Benchmarks The scenario runner starts local gateway and worker processes, sends load, then writes JSON and markdown results. diff --git a/docs/deployment.md b/docs/deployment.md index 57abe8e..5831469 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -22,7 +22,7 @@ For a hosted demo, deploy the services as a private internal mesh: | Service | Public | Notes | | --- | --- | --- | -| gateway | yes | Exposes `/predict`, `/dashboard`, `/metrics`, and worker control proxy | +| gateway | yes | Exposes `/predict`, `/dashboard`, `/metrics`, and `/workers` | | worker-a | no | Internal service on port 9000 | | worker-b | no | Internal service on port 9000 | | redis | no | Internal cache only | @@ -35,13 +35,15 @@ REDIS_URL=redis://redis:6379/0 REQUEST_TIMEOUT_SECONDS=1.5 ROUTER_STRATEGY=load_aware CACHE_ENABLED=true +GATEWAY_CONTROLS_ENABLED=false ``` ## What not to overclaim This is not a production ML platform yet. It is a compact inference mesh that demonstrates training, serving, caching, routing, live worker controls, failure recovery, and benchmark evidence. -The next real hosting step is choosing a provider and wiring its private service networking. The Docker setup here is intentionally provider-neutral so the app can go to Fly.io, Render, Railway, a VM, or a Kubernetes cluster without changing the Python code. +The next real hosting step is applying the Render Blueprint and validating the gateway URL with +`scripts/smoke_check.py`. ## Render Blueprint @@ -53,4 +55,6 @@ The next real hosting step is choosing a provider and wiring its private service The gateway reads `WORKER_A_HOSTPORT` and `WORKER_B_HOSTPORT`, which Render fills from each private worker service. That avoids hardcoding internal hostnames. +The Blueprint also sets `GATEWAY_CONTROLS_ENABLED=false`, so the public dashboard can show worker state without exposing delay or failure controls. + Important: the private worker services use Render's `starter` plan in the Blueprint because private services do not run on the free plan. Review the cost in Render before applying the Blueprint. diff --git a/docs/hosted-demo.md b/docs/hosted-demo.md new file mode 100644 index 0000000..28ab1ee --- /dev/null +++ b/docs/hosted-demo.md @@ -0,0 +1,44 @@ +# Hosted demo + +ModelMesh uses the Render Blueprint in `render.yaml` for the public demo shape: + +- one public gateway +- two private workers +- one managed cache + +The hosted gateway exposes `/health`, `/predict`, `/metrics`, `/workers`, and `/dashboard`. +Worker delay and failure controls are disabled in the Blueprint with +`GATEWAY_CONTROLS_ENABLED=false`. + +## Deploy from Render + +1. Open Render and choose **New** > **Blueprint**. +2. Connect this repository. +3. Select `render.yaml`. +4. Review the service list before applying: + - `modelmesh-gateway` uses the `starter` plan. + - `modelmesh-worker-a` and `modelmesh-worker-b` use private services. + - `modelmesh-cache` uses Render Key Value. +5. Apply the Blueprint. +6. Wait until the gateway and both workers finish their first deploy. + +Private worker services may cost money on Render. Review the pricing screen before applying +the Blueprint. + +## Validate the live gateway + +After Render gives the gateway a public URL, run: + +```bash +python scripts/smoke_check.py --base-url https://your-render-gateway-url +``` + +The smoke check verifies: + +- `/health` +- `/predict` +- `/metrics` +- `/workers` +- `/dashboard` + +Only add the live URL to the README or GitHub repo homepage after this smoke check passes. diff --git a/docs/report.md b/docs/report.md index c4dbcc5..85d5129 100644 --- a/docs/report.md +++ b/docs/report.md @@ -119,8 +119,9 @@ The repo includes: - local Docker Compose - production-style Docker Compose - Docker image builds in CI +- Render Blueprint for a hosted demo -A separate Render Blueprint PR prepares the hosted demo. It still needs a provider account action and cost review before there is a public URL. +The hosted demo still needs a Render account action and cost review before there is a public URL. The Blueprint keeps workers private and disables public worker failure controls. ## Limitations diff --git a/gateway/app/dashboard.py b/gateway/app/dashboard.py index 27f862d..91f178f 100644 --- a/gateway/app/dashboard.py +++ b/gateway/app/dashboard.py @@ -113,6 +113,9 @@ .bad { color: #fca5a5; } + .muted { + color: #64748b; + } input { width: 76px; border: 1px solid #334155; @@ -215,6 +218,7 @@ errors: document.getElementById("errors"), workers: document.getElementById("workers"), }; + const controlsEnabled = __CONTROLS_ENABLED__; function pct(value) { return `${Math.round(value * 100)}%`; @@ -234,6 +238,16 @@ window.controlWorker = controlWorker; + function controlCell(worker) { + if (!controlsEnabled) { + return `disabled`; + } + return ` + + + `; + } + async function refresh() { try { const [health, metrics, workers] = await Promise.all([ @@ -247,6 +261,7 @@ ids.cache.textContent = pct(metrics.cache_hit_rate); ids.p95.textContent = `${metrics.p95_latency_ms} ms`; ids.errors.textContent = metrics.worker_errors; + const inputState = controlsEnabled ? "" : "disabled"; ids.workers.innerHTML = workers.map((worker) => `