From 24e5e9ce82a15b2d7472424a783264e2e889918a Mon Sep 17 00:00:00 2001 From: SimpleTest Date: Sat, 18 Jul 2026 22:55:20 +0300 Subject: [PATCH] feat: add isolated scaling measurements --- .env.example | 6 + .env.load.example | 53 ++++++++ .gitignore | 1 + README.md | 1 + compose.load.yaml | 19 +++ compose.yaml | 10 +- docs/dependency-baseline.md | 2 + docs/performance.md | 121 +++++++++++++++++ docs/verification.md | 7 +- load/k6/public-and-websocket.js | 154 ++++++++++++++++++++++ scripts/ensure-local-load-env.sh | 61 +++++++++ scripts/load-run.sh | 204 +++++++++++++++++++++++++++++ scripts/load-stack-stop.sh | 31 +++++ scripts/load-stack-up.sh | 63 +++++++++ scripts/verify-realtime-cluster.sh | 3 +- 15 files changed, 729 insertions(+), 7 deletions(-) create mode 100644 .env.load.example create mode 100644 compose.load.yaml create mode 100644 docs/performance.md create mode 100644 load/k6/public-and-websocket.js create mode 100755 scripts/ensure-local-load-env.sh create mode 100755 scripts/load-run.sh create mode 100755 scripts/load-stack-stop.sh create mode 100755 scripts/load-stack-up.sh diff --git a/.env.example b/.env.example index d28d034..847a453 100644 --- a/.env.example +++ b/.env.example @@ -5,6 +5,12 @@ MAILPIT_PORT=8027 # Comma-separated proxy IP/CIDR values whose X-Forwarded-* headers Traefik # accepts. Keep loopback locally; set the exact VPN proxy address for staging. TRAEFIK_TRUSTED_IPS=127.0.0.1/32 +# Docker-provider isolation and names. A second Compose project must use its +# own project constraint, router/service name, Docker network, and Host rule. +TRAEFIK_PROJECT_CONSTRAINT=who_need_help +TRAEFIK_APP_NAME=who-need-help +TRAEFIK_DOCKER_NETWORK=who_need_help_internal +TRAEFIK_ROUTER_RULE='PathPrefix(`/`)' PHX_HOST=localhost PHX_SCHEME=http PHX_URL_PORT=4010 diff --git a/.env.load.example b/.env.load.example new file mode 100644 index 0000000..b1cb2be --- /dev/null +++ b/.env.load.example @@ -0,0 +1,53 @@ +# Copying is handled by scripts/ensure-local-load-env.sh, which replaces every +# GENERATE_* marker with an independent local secret and restricts the result +# to mode 600. This profile owns a separate Compose project and PostgreSQL +# volume; it must never use the staging project name. +LOAD_PROJECT=who_need_help_load +LOAD_HOST=load.local +HTTP_PORT=4110 +MAILPIT_PORT=8127 +PHX_HOST=load.local +PHX_SCHEME=https +PHX_URL_PORT=443 +TRAEFIK_TRUSTED_IPS=127.0.0.1/32 +TRAEFIK_PROJECT_CONSTRAINT=who_need_help_load +TRAEFIK_APP_NAME=who-need-help-load +TRAEFIK_DOCKER_NETWORK=who_need_help_load_internal +TRAEFIK_ROUTER_RULE='Host(`load.local`)' + +POSTGRES_DB=who_need_help_load +POSTGRES_USER=wnh_load +POSTGRES_PASSWORD=GENERATE_POSTGRES_PASSWORD +DATABASE_URL=GENERATE_DATABASE_URL +POOL_SIZE=10 +SECRET_KEY_BASE=GENERATE_SECRET_KEY_BASE +HANDOVER_SECRET=GENERATE_HANDOVER_SECRET +RELEASE_COOKIE=GENERATE_RELEASE_COOKIE +METRICS_TOKEN=GENERATE_METRICS_TOKEN + +SMTP_RELAY=mailpit +SMTP_PORT=1025 +SMTP_USERNAME= +SMTP_PASSWORD= +SMTP_AUTH=never +SMTP_TLS=never +SMTP_SSL=false +EMAIL_FROM_NAME="Who Need Help load profile" +EMAIL_FROM_ADDRESS=load-profile@example.invalid + +CODEX_SESSION_ID=load-profile +RATE_LIMIT_POLICIES_JSON={} +MAP_TILE_URL=https://tile.openstreetmap.org/{z}/{x}/{y}.png +GITHUB_OAUTH_CLIENT_ID= +GITHUB_OAUTH_CLIENT_SECRET= + +# These are reproducible experiment inputs, not capacity requirements, +# production traffic forecasts, alert thresholds, or recommended limits. +LOAD_WEB_REPLICAS=2 +LOAD_WORKER_REPLICAS=2 +LOAD_HTTP_VUS=40 +LOAD_WS_VUS=40 +LOAD_DURATION=30s +LOAD_WS_HOLD_MS=5000 +LOAD_WS_CONNECT_TIMEOUT_MS=3000 +LOAD_HTTP_THINK_SECONDS=0.1 diff --git a/.gitignore b/.gitignore index 08ded93..9abe519 100644 --- a/.gitignore +++ b/.gitignore @@ -43,6 +43,7 @@ npm-debug.log /.env /.env.* !/.env.example +!/.env.load.example # Local browser automation state and generated verification artifacts. /.playwright-cli/ diff --git a/README.md b/README.md index 9312a7b..7c8ef1b 100644 --- a/README.md +++ b/README.md @@ -232,6 +232,7 @@ and no fallback provider. Recommendations require a human moderator action. - [Architecture](docs/architecture.md) - [Trust and safety](docs/trust-safety.md) - [Operations runbook](docs/operations.md) +- [Performance measurement](docs/performance.md) - [Implementation verification and known limits](docs/verification.md) - [Verified dependency baseline](docs/dependency-baseline.md) - [PostgreSQL/PostGIS ADR](docs/decisions/0001-postgresql-postgis-over-spacetimedb.md) diff --git a/compose.load.yaml b/compose.load.yaml new file mode 100644 index 0000000..13eaabe --- /dev/null +++ b/compose.load.yaml @@ -0,0 +1,19 @@ +services: + proxy: + networks: + edge: + aliases: + - ${LOAD_HOST} + internal: {} + + migrate: + image: who-need-help:load + + web: + image: who-need-help:load + labels: + - traefik.http.routers.${TRAEFIK_APP_NAME}.middlewares=${TRAEFIK_APP_NAME}-forwarded + - traefik.http.middlewares.${TRAEFIK_APP_NAME}-forwarded.headers.customrequestheaders.X-Forwarded-Proto=https + + worker: + image: who-need-help:load diff --git a/compose.yaml b/compose.yaml index 2e05505..df8f31e 100644 --- a/compose.yaml +++ b/compose.yaml @@ -35,6 +35,7 @@ services: - --api.dashboard=false - --providers.docker=true - --providers.docker.exposedbydefault=false + - --providers.docker.constraints=Label(`com.docker.compose.project`,`${TRAEFIK_PROJECT_CONSTRAINT:-who_need_help}`) - --entrypoints.web.address=:80 - --entrypoints.web.forwardedheaders.trustedips=${TRAEFIK_TRUSTED_IPS:-127.0.0.1/32} ports: @@ -92,10 +93,11 @@ services: condition: service_completed_successfully labels: - traefik.enable=true - - traefik.docker.network=who_need_help_internal - - traefik.http.routers.who-need-help.rule=PathPrefix(`/`) - - traefik.http.routers.who-need-help.entrypoints=web - - traefik.http.services.who-need-help.loadbalancer.server.port=4000 + - traefik.docker.network=${TRAEFIK_DOCKER_NETWORK:-who_need_help_internal} + - traefik.http.routers.${TRAEFIK_APP_NAME:-who-need-help}.rule=${TRAEFIK_ROUTER_RULE:-PathPrefix(`/`)} + - traefik.http.routers.${TRAEFIK_APP_NAME:-who-need-help}.entrypoints=web + - traefik.http.routers.${TRAEFIK_APP_NAME:-who-need-help}.service=${TRAEFIK_APP_NAME:-who-need-help} + - traefik.http.services.${TRAEFIK_APP_NAME:-who-need-help}.loadbalancer.server.port=4000 healthcheck: test: ["CMD", "curl", "--fail", "--silent", "http://localhost:4000/healthz/ready"] interval: 10s diff --git a/docs/dependency-baseline.md b/docs/dependency-baseline.md index aaa8efd..8fbc611 100644 --- a/docs/dependency-baseline.md +++ b/docs/dependency-baseline.md @@ -37,6 +37,7 @@ package checksums are in `mix.lock` and `assets/package-lock.json`. | PostGIS | 3.6.4 | | Traefik | 3.7.8 | | Mailpit | 1.30.4 | +| k6 load generator | 2.1.0 | | Debian builder/runner snapshot | trixie-20260713-slim | Every external Compose/kind service image and every Dockerfile base image is @@ -74,6 +75,7 @@ because the official SDK channel identifies it as a QPR beta. - [PostGIS release notes](https://postgis.net/docs/en/release_notes.html) - [Traefik releases](https://github.com/traefik/traefik/releases) - [Mailpit releases](https://github.com/axllent/mailpit/releases) +- [k6 releases](https://github.com/grafana/k6/releases) - [Android Gradle Plugin 9.3.0 release notes](https://developer.android.com/build/releases/agp-9-3-0-release-notes) - [Android 17 SDK setup](https://developer.android.com/about/versions/17/setup-sdk) - [Gradle release notes](https://docs.gradle.org/current/release-notes.html) diff --git a/docs/performance.md b/docs/performance.md new file mode 100644 index 0000000..78c285d --- /dev/null +++ b/docs/performance.md @@ -0,0 +1,121 @@ +# Performance measurement + +No production capacity, minimum resource requirement, SLO, alert threshold, +pool size, or autoscaling threshold is known yet. The repository therefore +contains a reproducible measurement profile, not a capacity claim or blocking +resource preflight. + +The profile uses a separate Compose project, generated independent secrets, and +a separate PostgreSQL volume. Its Traefik instance is constrained to that exact +Compose project; its router, service, middleware, and Host rule are unique, so +the load profile cannot join the staging router. It exercises: + +- dynamic public home and Safety pages; +- database-backed readiness queries; +- Phoenix WebSocket upgrades and protocol heartbeats; +- the configured number of web and worker replicas; +- cross-node Phoenix PubSub after each run. + +It does not authenticate users, create requests, send chat messages, update +locations, render a real browser, or reproduce an unknown future production +traffic mix. It also does not sample the k6 load-generator container itself. +Its results only describe the recorded host, container versions, replica +counts, database state, and experiment inputs. + +## Create the isolated profile + +```sh +./scripts/ensure-local-load-env.sh +./scripts/load-stack-up.sh +``` + +The generated `.env.load` is ignored, restricted to mode 600, and contains +independent PostgreSQL and application secrets. Edit its `LOAD_*` inputs to +define a specific experiment. Values in `.env.load.example` are reproducible +measurement points, not recommendations. + +## Run and compare replica counts + +```sh +./scripts/load-run.sh two-web +./scripts/load-stack-up.sh 3 +# Set LOAD_WEB_REPLICAS=3 in .env.load so the recorded expected topology +# matches the running topology, then: +./scripts/load-run.sh three-web +``` + +Each run writes ignored evidence below `output/performance/