Документация для следующего разработчика (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>
70 lines
2.7 KiB
JavaScript
70 lines
2.7 KiB
JavaScript
// 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,
|
||
});
|
||
}
|