food-market/tests/load/signup-burst.js
nns 97e26a65d5 docs(s12): ARCHITECTURE/MULTI-TENANCY/RUNBOOK/DEVELOPER-GUIDE + k6 baseline + stage-verify CI
Документация для следующего разработчика (4 файла, ~1500 строк по
существу), реальный нагрузочный baseline на stage, и автоматический
smoke на каждый push.

Доки:
- docs/ARCHITECTURE.md — карта слоёв, модулей, Program.cs composition
  root, полный поток signup→post с трассировщиком ASP.NET pipeline.
- docs/MULTI-TENANCY.md — ITenantEntity + reflection query-filter,
  stamping в SaveChanges, SuperAdmin override (read-only + edit-mode
  с reason), 8 подводных камней, чеклист «как добавить tenant-сущность».
- docs/RUNBOOK.md — health-чеки, backup/restore с примером, смена SDK,
  disaster-recovery на новый сервер, 6 описанных инцидентов
  (включая docker-compose project name), БД-troubleshooting.
- docs/DEVELOPER-GUIDE.md — локальный setup, гочи integration-тестов,
  полные паттерны (controller с permission + tenant-сущность с
  RowVersion + 5 шагов миграции), валидация, structured-логирование,
  «НЕ делать» список.

k6 baseline:
- tests/load/ — 3 скрипта (signup-burst, retail-sales-parallel,
  sales-report-heavy) + README с инструкциями.
- docs/performance-baseline.md — реальные цифры на stage:
  * signup p95 446ms @ 50 RPM (IP-лимит 60/мин держит);
  * retail-sale sequential — 17/sec, p95 71ms;
  * retail-sale @ VU>1 — 53% failure из-за race в
    GenerateNumberAsync (unique-violation 23505 не ловится в
    SaveOrFkErrorAsync) — P0 для следующего рефакторинга;
  * reports на 1500 чеков — p95 50-114ms до VU=5.

CI:
- .forgejo/workflows/stage-verify.yml — on workflow_run после Docker
  API/Web, wait-for-ready → tests/stage-smoke.sh → Telegram пинг.
- tests/stage-smoke.sh — 7-секундный bash-смок (curl+jq+python3),
  5 этапов: health, signup, token, multi-tenant изоляция (B → 404
  на product A, B → пустой список), полный документ-цикл
  (supplier+supply.post → stock=100 → sale.post → stock=99).
  Локальный прогон против stage — все этапы зелёные.

Build чистый, локальный прогон smoke зелёный. Sprint 12 закрывает
автономно-безопасный цикл — дальше нужен вход от user'а.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-07 03:19:25 +05:00

70 lines
2.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// signup-burst.js — нагрузочный тест регистрации новых tenant'ов.
//
// Сценарий: 100 signup'ов в минуту, ramping VU от 1 до 20 и обратно.
//
// ВАЖНО: на stage IP-лимит rate-limiter'а — 60/мин на /api/auth/signup
// и /connect/token (DefaultPerIpPerMinute=60, см.
// AuthRateLimiterExtensions). Запуская 100/мин с одного IP, мы УПРЁМСЯ
// в 429 — это нормальный исход теста. Метрика «процент 429» показывает,
// насколько IP-лимит держит вход; метрика «латенция при <60 rps» —
// чистая производительность стэка.
//
// Чтобы по-честному померить bootstrap БЕЗ упирания в лимит — уменьшить
// до 50 RPS (через TARGET_RPM=50).
import http from 'k6/http';
import { check } from 'k6';
import { Trend, Rate } from 'k6/metrics';
const BASE_URL = __ENV.BASE_URL || 'http://localhost:5081';
const TARGET_RPM = Number(__ENV.TARGET_RPM || 100);
const DURATION_S = 60;
const PER_SECOND = TARGET_RPM / 60;
const signupTrend = new Trend('signup_duration_ms', true);
const signup429 = new Rate('signup_rate_limited');
export const options = {
scenarios: {
signup_burst: {
executor: 'constant-arrival-rate',
rate: TARGET_RPM,
timeUnit: '1m',
duration: `${DURATION_S}s`,
preAllocatedVUs: 20,
maxVUs: 40,
},
},
thresholds: {
// Прагматичные пороги. p95 на dev-stack обычно < 1.5с для signup
// (создание Organization + User + Employee + Store + RetailPoint).
http_req_duration: ['p(95)<3000', 'p(99)<6000'],
// 429 — допустимо, но не должно быть >50% (тогда тест не информативен).
signup_rate_limited: ['rate<0.7'],
},
};
export default function () {
const id = `${__VU}-${__ITER}-${Date.now()}`;
const email = `load-signup-${id}@example.kz`;
const payload = JSON.stringify({
email,
password: 'Passw0rd!',
organizationName: `LoadOrg-${id}`,
phone: '+77001234567',
plan: null,
});
const t0 = Date.now();
const res = http.post(`${BASE_URL}/api/auth/signup`, payload, {
headers: { 'Content-Type': 'application/json' },
tags: { name: 'signup' },
});
signupTrend.add(Date.now() - t0);
signup429.add(res.status === 429);
check(res, {
'status is 2xx or 429': (r) => r.status >= 200 && r.status < 300 || r.status === 429,
});
}