Skip to content

Load tests (nightly) #121

Load tests (nightly)

Load tests (nightly) #121

Workflow file for this run

name: Load tests (nightly)
# Phase 5 M-P9 piece E — nightly load test against a fresh local
# stack (DynamoDB Local + MinIO + Redis + axum server). Goose driver
# is in tests/load/ and is excluded from the main workspace so its
# reqwest 0.11 pin doesn't enter the production build graph.
#
# The job uploads the Goose HTML report + JSON gate summary as
# artifacts. A failing run blocks no merges (nightly only, not on
# PR push) — the alarms in `runbook/sla-alarms.md` (M-P9 piece F)
# are the merge-gating signal; this workflow gives engineers an
# observable trend before alarms fire.
on:
schedule:
# 04:30 UTC — after the audit-retention and trash-cleanup
# workers (03 / 04 UTC respectively), before the digest
# scheduler (15 UTC).
- cron: "30 4 * * *"
workflow_dispatch:
inputs:
users:
description: "Concurrent users (defaults to 50 for the smoke run)"
required: false
default: "50"
run_time:
description: "Total duration (e.g. 60s, 5m)"
required: false
default: "60s"
concurrency:
group: load-tests-${{ github.ref }}
cancel-in-progress: false
env:
CARGO_TERM_COLOR: always
RUST_BACKTRACE: 1
AWS_ACCESS_KEY_ID: minioadmin
AWS_SECRET_ACCESS_KEY: minioadmin
AWS_REGION: us-east-1
AWS_ENDPOINT_URL_DYNAMODB: http://127.0.0.1:8000
AWS_ENDPOINT_URL_S3: http://127.0.0.1:9000
DYNAMODB_TABLE_PREFIX: "load-"
S3_BUCKET: "load-ogrenotes"
REDIS_URL: "redis://127.0.0.1:6379"
DEV_MODE: "true"
JWT_SECRET: "load-test-jwt-secret-that-is-at-least-32-bytes-long"
OAUTH_CLIENT_ID: "load-not-used"
OAUTH_CLIENT_SECRET: "load-not-used"
OAUTH_REDIRECT_URI: "http://localhost:3000/auth/callback"
SEARCH_INDEX_PATH: "/tmp/ogrenotes-load-search"
API_PORT: "3000"
# Bump the dev-login rate-limit ceiling — the load test mints
# one user per goose worker, and 50 logins in a 5-second hatch
# window would trip the default 100/min cap on a re-run.
RATE_LIMIT_DEV_LOGIN_PER_MIN: "10000"
jobs:
load-test:
name: read_heavy (${{ github.event.inputs.users || '50' }} users)
runs-on: ubuntu-latest
timeout-minutes: 25
steps:
- uses: actions/checkout@v4
- name: Install xmlsec1 + clang (samael dependencies)
run: bash scripts/ci-install-native-deps.sh
- name: Install Rust (stable)
uses: dtolnay/rust-toolchain@stable
- name: Cache cargo
uses: Swatinem/rust-cache@v2
with:
env-vars: LIBCLANG_PATH OGRE_NATIVE_ABI
workspaces: |
. -> target
tests/load -> target
shared-key: load-tests
- name: Start infra (DynamoDB / MinIO / Redis)
run: docker compose up -d
- name: Wait for services
run: |
for port in 8000 9000 6379; do
for i in $(seq 1 30); do
nc -z 127.0.0.1 "$port" && break
sleep 1
done
done
- name: Provision DynamoDB table + S3 bucket
run: cargo run --release --locked --bin setup_dev -p ogrenotes-api
- name: Build axum server (release, with dev-login)
run: cargo build --release --locked --bin ogrenotes-api --features dev-login
- name: Start axum server
run: |
mkdir -p /tmp/load-logs
./target/release/ogrenotes-api > /tmp/load-logs/server.log 2>&1 &
echo $! > /tmp/load-logs/server.pid
- name: Wait for server
run: |
for i in $(seq 1 60); do
if curl -sf http://127.0.0.1:3000/health >/dev/null; then
exit 0
fi
sleep 2
done
tail -n 200 /tmp/load-logs/server.log
exit 1
- name: Build goose driver
working-directory: tests/load
run: cargo build --release --locked
- name: Verify load gates against stub server
working-directory: tests/load
run: cargo test --locked --test gates --bin load -- --test-threads=1
- name: Run read_heavy scenario
working-directory: tests/load
env:
USERS: ${{ github.event.inputs.users || '50' }}
RUN_TIME: ${{ github.event.inputs.run_time || '60s' }}
LOAD_SUMMARY_FILE: ../../artifacts/load/gate-summary.json
LOAD_MIN_SUCCESSFUL_LOGINS: ${{ github.event.inputs.users || '50' }}
run: |
mkdir -p ../../artifacts/load
./target/release/load \
--host http://127.0.0.1:3000 \
--scenarios readheavy \
--users "$USERS" \
--hatch-rate 5 \
--run-time "$RUN_TIME" \
--report-file ../../artifacts/load/goose-report.html \
--no-reset-metrics
- name: Stop axum server
if: always()
run: |
if [ -f /tmp/load-logs/server.pid ]; then
kill "$(cat /tmp/load-logs/server.pid)" || true
fi
- name: Upload load-test artifacts
if: always()
uses: actions/upload-artifact@v4
with:
name: load-test-artifacts
path: |
artifacts/load/**
/tmp/load-logs/server.log
if-no-files-found: warn
retention-days: 30
- name: Compose logs on failure
if: failure()
run: docker compose logs --no-color
- name: Stop infra
if: always()
run: docker compose down -v