Files
curo1305andClaude Sonnet 4.6 70c09f6cd4 docs(06): create phase 6 plan — performance & production hardening
6 plans across 4 waves covering structlog/Loki observability, Locust
load testing, multi-stage Dockerfile hardening, trusted-proxy rate
limiting, and RUNBOOK.md. Verification passed (0 blockers).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-02 20:35:03 +02:00

17 KiB

phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, user_setup, must_haves
phase plan type wave depends_on files_modified autonomous requirements user_setup must_haves
06-performance-production-hardening 03 execute 1
06-01
backend/requirements-dev.txt
backend/load_tests/locustfile.py
backend/load_tests/README.md
false
D-04
D-05
D-06
service why env_vars
locust Load testing runs OUTSIDE the production Docker image; install on the host or in a dedicated venv
name source
LOAD_TEST_EMAIL Local .env or shell export — defaults to loadtest@example.com
name source
LOAD_TEST_PASSWORD Local .env or shell export — defaults to a fixed value matching AUTH-01 strength rules
truths artifacts key_links
locust is installable from requirements-dev.txt and is NOT installed by the production Dockerfile
locustfile.py exposes a DocuVaultUser HttpUser class with login → list → upload tasks weighted realistically
Running `locust --headless --users 50 --spawn-rate 10 --run-time 5m` against a running stack exits 0 only when p95 < 200ms AND p99 < 500ms AND fail_ratio ≤ 1%
Locust on_start is self-bootstrapping — calls /api/auth/register first (409 conflict ignored), then /api/auth/login
The upload task path matches the actual documents.py shape (presigned upload-url → PUT → confirm OR direct /upload — confirmed against documents.py before commit)
path provides contains
backend/requirements-dev.txt locust pin (separate from production requirements.txt) locust>=2.34.0
path provides contains min_lines
backend/load_tests/locustfile.py Full Locust user class + SLA gating quitting listener class DocuVaultUser 80
path provides
backend/load_tests/README.md Run instructions, prerequisites, env var documentation, expected exit codes
from to via pattern
backend/load_tests/locustfile.py on_start() /api/auth/register and /api/auth/login POST JSON {email, password, handle} auth/(register|login)
from to via pattern
backend/load_tests/locustfile.py upload_document() documents.py upload endpoint shape (presigned vs direct) verified by reading backend/api/documents.py first api/documents/(upload|upload-url)
from to via pattern
backend/load_tests/locustfile.py check_sla() environment.process_exit_code @events.quitting.add_listener events.quitting.add_listener
Implement the Locust load test (D-04, D-05, D-06) that satisfies the SLA targets in Phase 6 success criterion 1: 50 concurrent users for a 5-minute soak with p50 < 100ms, p95 < 200ms, p99 < 500ms, and zero endpoint failures. Promote the locustfile.py skeleton from 06-01 to a full implementation. Resolve RESEARCH.md Open Question 1 by reading documents.py and using the correct upload flow shape.

Purpose: Make Phase 6 SC-01 measurable and gate-able. Provide a single command (locust --headless ...) whose exit code tells the on-call engineer whether the latest deployment meets SLA.

Output: requirements-dev.txt with locust pinned, a complete locustfile.py with SLA-gating quitting listener, and a load_tests/README.md so a fresh operator can run the test without reading PLAN.md.

<execution_context> @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md </execution_context>

@.planning/phases/06-performance-production-hardening/06-CONTEXT.md @.planning/phases/06-performance-production-hardening/06-RESEARCH.md @.planning/phases/06-performance-production-hardening/06-PATTERNS.md @.planning/phases/06-performance-production-hardening/06-VALIDATION.md @CLAUDE.md @backend/api/documents.py @backend/api/auth.py @backend/tests/conftest.py @backend/load_tests/locustfile.py @backend/load_tests/__init__.py

From backend/api/auth.py:

  • POST /api/auth/register — body {handle, email, password}, returns 201 on success or 409 if email exists.
  • POST /api/auth/login — body {email, password, totp_code?, backup_code?}, returns 200 with {access_token: str, ...} on success.
  • POST /api/auth/refresh — relies on the httpOnly refresh_token cookie which the Locust HttpUser session maintains automatically via the requests Session it wraps.

From backend/api/documents.py (verified at planning time):

  • GET /api/documents/ — list endpoint, returns array.
  • POST /api/documents/upload-url — body {filename, ...}, returns {upload_url, document_id}. Two-step presigned flow.
  • POST /api/documents/{id}/confirm — finalises after MinIO PUT, atomic quota UPDATE.
  • POST /api/documents/upload — alternative direct multipart flow used for cloud backends (also accepts target_backend=minio). Use the DIRECT /upload endpoint for the load test because it is a single HTTP call from the client's perspective and avoids needing a MinIO presigned PUT from the Locust process.

From RESEARCH.md Pattern 7 + Open Question 1 resolution:

  • Locust HttpUser maintains an HttpSession (cookies preserved across calls within one virtual user) — refresh_token cookie persists between login and refresh calls.
Task 1: Confirm load-test credential strategy - .planning/phases/06-performance-production-hardening/06-RESEARCH.md (Pitfall 4 — load test user not pre-created) - .planning/phases/06-performance-production-hardening/06-CONTEXT.md (D-04 prerequisites) Two strategies for the load-test user. The planner needs the operator to pick one before implementation. Option A — self-bootstrapping: locustfile on_start calls POST /api/auth/register (silently swallows 409 if user already exists), then POST /api/auth/login. Pros: zero out-of-band setup; works against a clean DB. Cons: writes a real user into the production-shape DB; password defaults to a fixed string unless LOAD_TEST_PASSWORD env var is set. Option B — pre-seeded user via Alembic seed: planner writes a one-off migration or makefile target to create the load-test user, locustfile only logs in. Pros: load test does not pollute the user table during runs. Cons: extra setup step; operator must run the seed before every fresh DB. 1. Decide: Option A (self-bootstrap, default) or Option B (pre-seeded). 2. If Option A: confirm the load-test handle/email default — the planner uses `loadtestuser` / `loadtest@example.com` / `Loadtest123!@#` unless overridden by LOAD_TEST_EMAIL / LOAD_TEST_PASSWORD env vars. Confirm this password meets AUTH-01 strength rules: ≥12 chars, upper, lower, digit, special — "Loadtest123!@#" is 14 chars and has all classes. 3. If Option B: provide the seeding command you want the planner to document in README.md. - User selects "A" or "B"; planner proceeds with that strategy in Task 3. - If A: README.md documents that the loadtest user persists in the DB and can be cleaned up with `DELETE FROM users WHERE email='loadtest@example.com'`. - If B: README.md documents the seeding command before-each-run. Type "A" for self-bootstrap (default) or "B" for pre-seeded with the seeding command. Task 2: Add locust to requirements-dev.txt backend/requirements-dev.txt - backend/requirements.txt (current pins — do NOT modify this file) - .planning/phases/06-performance-production-hardening/06-RESEARCH.md (Standard Stack section — "Locust is a dev/test dependency only") Create backend/requirements-dev.txt (new file). Top of file: comment block stating "Development and load-test dependencies. NOT installed by the production Dockerfile. Install separately on the host or in a dedicated venv via: `pip install -r backend/requirements-dev.txt`." First include line: `-r requirements.txt` so the dev environment also installs everything from production. Then a `# Load testing (Phase 6 — D-04)` comment followed by `locust>=2.34.0`. Do NOT modify backend/requirements.txt — locust must NOT enter the production image (per RESEARCH.md and the gates checklist). test -f backend/requirements-dev.txt && grep -c '^locust' backend/requirements-dev.txt && ! grep -q '^locust' backend/requirements.txt - File backend/requirements-dev.txt exists: `test -f backend/requirements-dev.txt` returns 0. - File contains `-r requirements.txt`: `grep -c '^-r requirements.txt' backend/requirements-dev.txt` returns 1. - File contains `locust>=2.34.0`: `grep -c '^locust>=2.34.0' backend/requirements-dev.txt` returns 1. - backend/requirements.txt does NOT contain locust: `grep -c '^locust' backend/requirements.txt` returns 0. - File parses as valid pip requirements: `pip install --dry-run -r backend/requirements-dev.txt 2>&1 | grep -cE 'ERROR|error'` returns 0. locust installable from requirements-dev.txt, production requirements.txt unchanged. Task 3: Implement locustfile.py + README.md backend/load_tests/locustfile.py, backend/load_tests/README.md - backend/api/documents.py (lines around `upload_document` and `confirm_upload` — confirm the request body shape for /upload and /upload-url + /{id}/confirm before finalising the upload task) - backend/api/auth.py (register/login request body shapes) - backend/load_tests/locustfile.py (the skeleton from 06-01 — replace bodies) - .planning/phases/06-performance-production-hardening/06-RESEARCH.md (Pattern 7, Pitfall 4, Open Question 1 + 2) - .planning/phases/06-performance-production-hardening/06-PATTERNS.md (locustfile.py section) Replace the body of backend/load_tests/locustfile.py with the full Pattern 7 implementation, parameterised by the credential strategy chosen in Task 1. Top of file docstring: name, purpose, run command (`locust --headless --users 50 --spawn-rate 10 --run-time 5m --host http://localhost:8000 --csv backend/load_tests/results -f backend/load_tests/locustfile.py`), prerequisites, SLA exit-code semantics. Imports: `import os`, `from io import BytesIO`, `from locust import HttpUser, task, between, events`. NO imports from backend application code. Constants: TEST_EMAIL = os.environ.get("LOAD_TEST_EMAIL", "loadtest@example.com"); TEST_PASSWORD = os.environ.get("LOAD_TEST_PASSWORD", "Loadtest123!@#"); TEST_HANDLE = "loadtestuser". Class DocuVaultUser(HttpUser): wait_time = between(0.5, 2.0); access_token: str = "". Method on_start(self): if strategy A — POST /api/auth/register with {handle: TEST_HANDLE, email: TEST_EMAIL, password: TEST_PASSWORD} catching 409 silently; THEN POST /api/auth/login with {email, password}; on 200 set self.access_token from response JSON access_token; on any other status call self.environment.runner.quit(). Method _auth_headers(self): return {"Authorization": f"Bearer {self.access_token}"}. Tasks (weighted per D-05 realistic session): @task(5) list_documents(self): self.client.get("/api/documents/", headers=self._auth_headers(), name="GET /api/documents/"). @task(2) upload_document(self): use the DIRECT /api/documents/upload endpoint with form-data (target_backend="minio", file fake PDF bytes b"%PDF-1.4 ..." in a BytesIO), headers from _auth_headers, name="POST /api/documents/upload". Confirm before committing that documents.py /upload accepts the target_backend form field. @task(3) get_document(self): list first to pick an id; if list is non-empty pick the first doc and GET /api/documents/{id}, name="GET /api/documents/{id}"; if empty, skip silently (do not record as failure). @task(1) refresh_token(self): POST /api/auth/refresh, name="POST /api/auth/refresh" — relies on HttpSession cookie jar to carry refresh_token. SLA listener (module-level): `@events.quitting.add_listener def check_sla(environment, **kwargs):` reads environment.runner.stats.total; if fail_ratio > 0.01 → environment.process_exit_code = 1; elif p95 > 200 → 1; elif p99 > 500 → 1. Create backend/load_tests/README.md documenting: purpose, install command (`pip install -r backend/requirements-dev.txt`), run command (full headless example), env vars (LOAD_TEST_EMAIL, LOAD_TEST_PASSWORD with safe defaults), SLA semantics, exit-code mapping, cleanup tip for the loadtest user, and a note that load tests run OUTSIDE the production container. cd backend && pip install -q locust && python -c "import ast; ast.parse(open('load_tests/locustfile.py').read())" && cd backend && locust -f load_tests/locustfile.py --headless --users 1 --spawn-rate 1 --run-time 5s --host http://localhost:8000 --only-summary 2>&1 | tail -5 - locustfile.py is syntactically valid: `python -c "import ast; ast.parse(open('backend/load_tests/locustfile.py').read())"` exits 0. - `grep -c "class DocuVaultUser" backend/load_tests/locustfile.py` returns 1. - `grep -c "raise NotImplementedError" backend/load_tests/locustfile.py` returns 0 (all skeleton stubs replaced). - `grep -c "events.quitting.add_listener" backend/load_tests/locustfile.py` returns 1. - `grep -cE "p95|get_response_time_percentile.0\\.95" backend/load_tests/locustfile.py` returns ≥ 1. - `grep -cE "p99|get_response_time_percentile.0\\.99" backend/load_tests/locustfile.py` returns ≥ 1. - `grep -cE "^from (backend|api|services|db|deps)" backend/load_tests/locustfile.py` returns 0 (no application imports). - `grep -c "@task" backend/load_tests/locustfile.py` returns 4 (list, upload, get, refresh). - locust accepts the file without parse errors: `locust -f backend/load_tests/locustfile.py --list-commands 2>&1 | grep -cE 'error|Error|invalid'` returns 0. - README.md exists: `test -f backend/load_tests/README.md` returns 0. - README.md mentions LOAD_TEST_EMAIL: `grep -c LOAD_TEST_EMAIL backend/load_tests/README.md` returns ≥ 1. - The actual /api/documents/upload endpoint shape was confirmed against documents.py before commit (planner records this in SUMMARY). locustfile.py runs without parse errors, defines four weighted tasks, SLA listener gates exit code on p95/p99/fail_ratio, README.md explains how to operate it.

<threat_model>

Trust Boundaries

Boundary Description
Locust host process → backend API Same as any HTTP client; uses real auth tokens, exercises real rate limits
LOAD_TEST_PASSWORD env var → process memory Credential lives only in env vars and the HTTP session — never written to disk by locust

STRIDE Threat Register

Threat ID Category Component Disposition Mitigation Plan
T-06-03-01 Information Disclosure Load-test credentials hardcoded in version control mitigate TEST_EMAIL/TEST_PASSWORD read from env vars with non-sensitive defaults; README documents using a real password via LOAD_TEST_PASSWORD; .env already in .gitignore
T-06-03-02 Tampering Locust added to production Docker image mitigate locust placed in requirements-dev.txt only; production Dockerfile installs only requirements.txt; grep gate verifies locust is absent from requirements.txt
T-06-03-03 Denial of Service Load test against production by mistake accept Operator responsibility; --host flag is required for locust to run; README warns to use http://localhost:8000 or a dedicated staging URL
T-06-03-04 Tampering locustfile import of application code couples test harness to deployment mitigate grep gate enforces zero imports from backend/api/services/db/deps in locustfile.py
T-06-03-SC Tampering locust install from PyPI mitigate Package legitimacy verified in 06-01 Task 1 (blocking human checkpoint); install gated behind that approval
</threat_model>
- backend/requirements.txt has no `^locust` line. - backend/requirements-dev.txt has `^locust>=2.34.0`. - locustfile.py parses, defines DocuVaultUser, has four @task decorators, has the SLA quitting listener, and zero application imports. - README.md exists and documents the run command, env vars, SLA semantics. - Operator-confirmed credential strategy (A or B) recorded in SUMMARY.

<success_criteria>

  • D-04 satisfied: Locust is the load tester, file lives at backend/load_tests/locustfile.py, runnable headless.
  • D-05 satisfied: Tasks cover login (on_start) → list → get → upload, weighted realistically; cloud endpoints excluded.
  • D-06 satisfied: SLA listener exits non-zero when p95 > 200ms OR p99 > 500ms OR fail_ratio > 1%.
  • locust kept out of production image (gates checklist item). </success_criteria>
Create `.planning/phases/06-performance-production-hardening/06-03-SUMMARY.md` when done. Include: credential strategy chosen (A/B), the exact upload endpoint shape used (presigned three-step vs direct one-step) and why, full locustfile line count, sample dry-run output (5s run against a running backend).