# Locust Load Test Examples

Load test scenarios for zae-limiter using [Locust](https://locust.io/) with the
`RateLimiterUser` integration.

## Project Structure

Follows [Locust's recommended structure](https://docs.locust.io/en/stable/writing-a-locustfile.html):

```
examples/locust/
├── common/                     # Shared utilities
│   ├── config.py               # LoadConfig, LoadDistribution
│   └── distribution.py         # TrafficDistributor (whale/spike/powerlaw)
├── locustfiles/                # Scenario files
│   ├── simple.py               # Single resource, single limit
│   ├── max_rps.py              # Zero-wait max throughput benchmark
│   ├── llm_gateway.py          # LLM gateway with lease adjustments
│   ├── llm_production.py       # Production LLM traffic with load shapes
│   └── stress.py               # Whale/spike/power-law distribution
└── README.md
```

## Scenarios

| Scenario | Resources | Limits | Entities | Pattern |
|----------|-----------|--------|----------|---------|
| `simple` | 1 (`api`) | RPM | Many anonymous | `acquire` only |
| `max_rps` | 1 (`api`) | RPM | Many anonymous | Zero-wait back-to-back `acquire` |
| `llm_gateway` | 8 LLM models | RPM + TPM | Many anonymous | `acquire` → `adjust` → `commit` |
| `llm_production` | 8 LLM models | RPM + TPM | Many anonymous | Weighted tasks + custom load shapes |
| `stress` | 8 APIs | RPM + TPM | 16K (whale/spike/powerlaw) | Hot partition + burst testing |

### User Classes

Each locustfile may define multiple user classes. Use positional arguments to select
specific classes (default: all classes in the file).

| File | Class | Cascade | Description |
|------|-------|---------|-------------|
| `simple.py` | `SimpleUser` | No | Standalone entities, system-level RPM limit |
| `simple.py` | `SimpleCascadeUser` | Yes | Child entities under a shared parent |
| `max_rps.py` | `MaxRpsUser` | No | Zero-wait standalone, max throughput |
| `max_rps.py` | `MaxRpsCascadeUser` | Yes | Zero-wait cascade, measures cascade overhead |

Cascade classes create child entities with `cascade=True` under a shared parent
created in `on_test_start`. Every `acquire()` writes to both child and parent
buckets, enabling direct comparison of cascade overhead vs standalone operation.

## Running Locally

Run from this directory (`examples/locust/`):

```bash
# Simple: single resource, single limit
locust -f locustfiles/simple.py --host <stack-name>

# Max RPS: zero-wait throughput benchmark
locust -f locustfiles/max_rps.py --host <stack-name>

# Run only the cascade user class
locust -f locustfiles/max_rps.py --host <stack-name> MaxRpsCascadeUser

# LLM Gateway: multiple models with lease adjustments
locust -f locustfiles/llm_gateway.py --host <stack-name>

# LLM Production: realistic traffic with daily/spike load shapes
locust -f locustfiles/llm_production.py --host <stack-name>

# Stress: whale/spike/power-law traffic
locust -f locustfiles/stress.py --host <stack-name>

# All scenarios at once
locust -f locustfiles/ --host <stack-name>
```

Then open http://localhost:8089 to configure users and start the test.

## Running on AWS (Distributed)

The `zae-limiter loadtest` commands deploy a distributed Locust cluster using
ECS Fargate (master) + Lambda (workers).

### 1. Deploy Infrastructure

```bash
# Simple scenario
zae-limiter loadtest deploy -n <stack-name> -C examples/locust -f locustfiles/simple.py

# LLM Gateway scenario
zae-limiter loadtest deploy -n <stack-name> -C examples/locust -f locustfiles/llm_gateway.py

# Stress scenario (set up test data first — see step 2)
zae-limiter loadtest deploy -n <stack-name> -C examples/locust -f locustfiles/stress.py
```

The deploy command will interactively prompt for VPC and subnet selection if not
provided via `--vpc-id` and `--subnet-ids`.

Additional flags:

| Flag | Default | Description |
|------|---------|-------------|
| `--max-workers` | 100 | Maximum Lambda worker concurrency |
| `--lambda-timeout` | 5 | Lambda worker timeout in minutes |
| `--ssm-endpoint / --no-ssm-endpoint` | off | Create VPC endpoints for SSM (not needed if VPC has NAT gateway) |
| `--dynamodb-endpoint / --no-dynamodb-endpoint` | on | Create DynamoDB gateway endpoint (auto-discovers route tables) |

### 2. Set Up Test Data (stress scenario only)

The stress scenario expects pre-configured entities and limits:

```bash
zae-limiter loadtest deploy -n <stack-name> --apis 8 --custom-limits 300
```

The simple and llm_gateway scenarios configure their own limits via `on_start()`,
so no setup step is needed.

### 3. Connect to Locust UI

```bash
zae-limiter loadtest ui -n <stack-name> -f locustfiles/simple.py
```

This starts a Fargate task (if not running), waits for SSM agent readiness,
opens an SSM tunnel to http://localhost:8089, and auto-opens the browser
when the tunnel is ready.

| Flag | Default | Description |
|------|---------|-------------|
| `-f` / `--locustfile` | (required) | Locustfile path relative to `-C` directory |
| `--port` | 8089 | Local port for the Locust UI |
| `--destroy` | off | Stop Fargate task on disconnect even if already running |
| `--force` | off | Stop existing task and restart with new config |
| `--max-workers` | (from deploy) | Override max Lambda workers |
| `--min-workers` | (from deploy) | Override minimum workers |
| `--users-per-worker` | (from deploy) | Override users per worker ratio |

### 4. Run Headless Tests

```bash
# Lambda mode (default): single Lambda invocation
zae-limiter loadtest run -n <stack-name> -f locustfiles/max_rps.py --users 20 --duration 60

# Run only cascade user class (positional args)
zae-limiter loadtest run -n <stack-name> -f locustfiles/max_rps.py MaxRpsCascadeUser

# Distributed mode: Fargate master + Lambda workers
zae-limiter loadtest run -n <stack-name> -f locustfiles/max_rps.py --workers 10 --users 100

# Calibrate optimal per-worker concurrency
zae-limiter loadtest tune -n <stack-name> -f locustfiles/max_rps.py
zae-limiter loadtest tune -n <stack-name> -f locustfiles/max_rps.py MaxRpsCascadeUser
```

User class names are passed as positional arguments (Locust native style).
When omitted, all classes in the locustfile are used.

### 5. Delete Infrastructure

```bash
zae-limiter loadtest delete -n <stack-name> --yes
```
