--- phase: 06-performance-production-hardening plan: 03 type: execute wave: 1 depends_on: - 06-01 files_modified: - backend/requirements-dev.txt - backend/load_tests/locustfile.py - backend/load_tests/README.md autonomous: false requirements: - D-04 - D-05 - D-06 user_setup: - service: locust why: "Load testing runs OUTSIDE the production Docker image; install on the host or in a dedicated venv" env_vars: - name: LOAD_TEST_EMAIL source: "Local .env or shell export — defaults to loadtest@example.com" - name: LOAD_TEST_PASSWORD source: "Local .env or shell export — defaults to a fixed value matching AUTH-01 strength rules" must_haves: truths: - "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)" artifacts: - path: "backend/requirements-dev.txt" provides: "locust pin (separate from production requirements.txt)" contains: "locust>=2.34.0" - path: "backend/load_tests/locustfile.py" provides: "Full Locust user class + SLA gating quitting listener" contains: "class DocuVaultUser" min_lines: 80 - path: "backend/load_tests/README.md" provides: "Run instructions, prerequisites, env var documentation, expected exit codes" key_links: - from: "backend/load_tests/locustfile.py on_start()" to: "/api/auth/register and /api/auth/login" via: "POST JSON {email, password, handle}" pattern: "auth/(register|login)" - from: "backend/load_tests/locustfile.py upload_document()" to: "documents.py upload endpoint shape (presigned vs direct)" via: "verified by reading backend/api/documents.py first" pattern: "api/documents/(upload|upload-url)" - from: "backend/load_tests/locustfile.py check_sla()" to: "environment.process_exit_code" via: "@events.quitting.add_listener" pattern: "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. @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md @.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. ## 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 | - 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. - 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). 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).