|
| 1 | +# Approvio Load Tests |
| 2 | + |
| 3 | +This directory contains the load testing suite for Approvio, based on the strategy defined in [ADR-007 Load Testing Strategy](../../docs/ADR/007-load-testing-strategy.md). It uses [k6](https://k6.io/) as the load generation tool. |
| 4 | + |
| 5 | +## Architecture |
| 6 | + |
| 7 | +The load testing framework is split into two phases: |
| 8 | + |
| 9 | +1. **Seed Phase (`scripts/seed.ts`)**: A Node.js script that connects directly to the database via Prisma and the authentication system to pre-provision a realistic organizational topology (Orgs, Spaces, Groups, Templates, Users, Agents, and auth tokens). It outputs this topology to a `seed-data.json` file. |
| 10 | +2. **Execution Phase (`scripts/scenarios/*.ts`)**: k6 scripts that simulate Virtual Users (VUs) interacting with the system. These scripts consume the `seed-data.json` file to know which endpoints to hit, which tokens to use, and which IDs to reference. |
| 11 | + |
| 12 | +## Directory Structure |
| 13 | + |
| 14 | +- `scripts/`: Entrypoints for test execution. |
| 15 | + - `seed.ts`: Setup script to generate test data. |
| 16 | + - `scenarios/`: Individual k6 test scripts. |
| 17 | + - `workflow-crud.ts`: Tests the core create-vote-check flow. |
| 18 | +- `lib/`: Shared helper modules for k6 scripts. |
| 19 | + - `auth.ts`: Manages picking random pre-generated tokens. |
| 20 | + - `data-gen.ts`: Generates synthetic payloads (unique workflow names, etc). |
| 21 | + - `checks.ts`: Custom k6 response validation. |
| 22 | +- `config/`: JSON files defining k6 execution profiles (VUs, duration, arrival rate). |
| 23 | + |
| 24 | +## Prerequisites |
| 25 | + |
| 26 | +- [k6](https://k6.io/docs/get-started/installation/) must be installed locally. |
| 27 | +- The Approvio backend API must be running. |
| 28 | +- The Approvio PostgreSQL database must be accessible for the seed script. |
| 29 | + |
| 30 | +## Running Tests |
| 31 | + |
| 32 | +First, generate the seed data: |
| 33 | + |
| 34 | +```bash |
| 35 | +yarn load-test:seed |
| 36 | +``` |
| 37 | + |
| 38 | +Then, run a test profile using the generic runner: |
| 39 | + |
| 40 | +```bash |
| 41 | +yarn load-test <config_name> <scenario_name> |
| 42 | +``` |
| 43 | + |
| 44 | +For convenience, shortcuts for standard profiles are defined: |
| 45 | + |
| 46 | +```bash |
| 47 | +yarn load-test:smoke # equivalent to: yarn load-test smoke workflow-crud |
| 48 | +yarn load-test:baseline # equivalent to: yarn load-test baseline workflow-crud |
| 49 | +yarn load-test:stress # equivalent to: yarn load-test stress workflow-crud |
| 50 | +``` |
| 51 | + |
| 52 | +## Profiles |
| 53 | + |
| 54 | +- **Smoke (`smoke.json`)**: Used to quickly verify that scripts, seed data, and target endpoints are functioning correctly. |
| 55 | +- **Baseline (`baseline.json`)**: Sustained load to establishes reference latency/throughput baselines. |
| 56 | +- **Stress (`stress.json`)**: Ramps up throughput over 10 minutes (up to 200 max VUs). Used to find the system's breaking point and resource limits. |
| 57 | + |
| 58 | +## Configuration Concepts |
| 59 | + |
| 60 | +k6 profiles are defined under `config/*.json` and configure how the test scenario generates load. Key concepts include: |
| 61 | + |
| 62 | +### 1. Executors |
| 63 | +An executor is the engine that drives the k6 run. We use different executors depending on the test profile: |
| 64 | +- **`constant-vus`** (used in Smoke): A fixed number of Virtual Users run as many iterations as they can for a specified duration. This is a *closed-loop* model where load generation depends on how fast the system under test responds. |
| 65 | +- **`constant-arrival-rate`** (used in Baseline): k6 starts a fixed number of iterations (`rate`) per time unit (e.g., `1s`), dynamically scaling the number of VUs up to `maxVUs` as needed to maintain that rate. This is an *open-loop* model, which prevents **coordinated omission** (where slow responses from a struggling system artificially lower the load generator's request rate). |
| 66 | +- **`ramping-arrival-rate`** (used in Stress): Iterations are started at a changing rate that ramps up or down through defined stages. |
| 67 | + |
| 68 | +### 2. Virtual Users (VUs) |
| 69 | +VUs are concurrent, independent execution loops. |
| 70 | +- In closed-loop executors (`constant-vus`), the VU count is fixed. |
| 71 | +- In open-loop executors (`constant-arrival-rate`/`ramping-arrival-rate`), VUs are allocated dynamically up to `maxVUs` to sustain the target iteration start rate. We pre-allocate a starting number (`preAllocatedVUs`) to avoid initialization overhead during the test. |
| 72 | + |
| 73 | +### 3. Rate & TimeUnit |
| 74 | +Used by arrival-rate executors to specify the target throughput: |
| 75 | +- **`rate`**: Number of iterations to start per time unit. |
| 76 | +- **`timeUnit`**: The period for the target rate (typically `"1s"`). |
| 77 | + |
| 78 | +### 4. Thresholds |
| 79 | +Pass/fail criteria evaluated on metrics. For example: |
| 80 | +- `http_req_failed`: The percentage of failed requests must remain under a threshold (e.g., `"rate<0.01"` for < 1% errors). |
| 81 | +- `http_req_duration`: Latency percentiles (e.g., `"p(95)<1000"` meaning 95% of requests must complete in under 1000ms). |
| 82 | + |
| 83 | +## Reading and Interpreting Results |
| 84 | + |
| 85 | +At the end of a test run, k6 prints a summary report in the console and saves detailed outputs to `load-tests/report.html` and `load-tests/summary.json`. Here is how to understand and interpret key metrics: |
| 86 | + |
| 87 | +### 1. Iterations vs. HTTP Requests |
| 88 | +- **`iterations`**: Represents the execution of the main scenario function (`export default function() { ... }`). In our case, one iteration simulates a complete user journey (e.g., creating a workflow, fetching its status, and submitting a vote). |
| 89 | +- **`iters/s` (or iteration rate)**: The number of completed user scenarios per second. |
| 90 | +- **`http_reqs` (and request rate)**: The actual HTTP request rate (RPS). Because each iteration contains 3 HTTP requests (POST `/workflows` + GET `/workflows/{id}` + POST `/workflows/{id}/vote`), your **RPS is roughly `3 * iters/s`** (excluding retries/pre-flight checks). |
| 91 | + |
| 92 | +### 2. Identifying Performance Bottlenecks |
| 93 | + |
| 94 | +Look for these key indicators in the report to evaluate system health: |
| 95 | + |
| 96 | +- **`dropped_iterations`**: If this number is greater than `0`, it means k6 tried to start new iterations to meet the target arrival rate (e.g., 20 iters/s) but was blocked because all VUs up to `maxVUs` were busy. This is a primary indicator that the backend is processing requests too slowly and has saturated the allocated VU pool. |
| 97 | +- **`http_req_duration`**: |
| 98 | + - `med (median/p50)`: The middle response time (under typical conditions). |
| 99 | + - `p(95)` and `p(99)`: Tail latencies representing the worst-case 5% and 1% of requests. Spikes here indicate database locks, Node.js event-loop blocking, or queue delays. |
| 100 | +- **`http_req_failed`**: The rate of HTTP requests that returned error status codes (5xx). Note that expected non-5xx errors under load (such as 409 OCC conflicts or 429 rate limits) are tracked separately from failed requests. |
| 101 | +- **`http_req_waiting`**: Time spent waiting for the first byte of response (often server-side processing/database query execution time). If this dominates `http_req_duration`, the bottleneck is server-side CPU or database CPU/locking, not network transport. |
| 102 | + |
| 103 | + |
0 commit comments