Нормализовать переводы строк в LF

Решение по TD-STYLE-ANALYZERS: LF — инструменты проекта (Python/Node) пишут LF,
CRLF-.sh не работают на Linux CI (sh scripts/ci.sh), большинство файлов уже были
LF. Добавлен .gitattributes (* text=auto eol=lf, бинарные исключения),
.editorconfig переведён на lf, 1029 файлов конвертированы, git add --renormalize.
Из индекса убраны закравшиеся archive/**/__pycache__/*.pyc.
This commit is contained in:
Rustam Khalimov
2026-09-11 19:01:42 +03:00
parent 39c9bdc1b7
commit 713d554dc2
751 changed files with 94507 additions and 94486 deletions
+54 -54
View File
@@ -1,54 +1,54 @@
# Нагрузочный смоук-тест API Deal (`scripts/loadtest/`)
Минимальный прогон «есть ли жизнь и как быстро отвечает» по читающим ручкам API
(этап 12, пакет C). Внешних сервисов и записывающих операций нет — только логин
и чтение, БД не мутируется.
## Сценарий
1. `POST /api/auth/login` `{login, password}` — сессия (`deal_session`).
2. В цикле: `GET /api/containers`, затем `GET /api/cards`.
3. Метрики: число запросов, ошибки (не-200), RPS, средняя/p95/max латентность.
Учётные данные по умолчанию — `admin` / `admin` (dev-дефолт bootstrap тенанта).
## Вариант A — bash + curl (зависимостей нет, кроме curl)
```bash
bash scripts/loadtest/api-loadtest.sh
# пример с параметрами
BASE_URL=http://localhost:5080 LOGIN=admin PASSWORD=admin \
DURATION_SECONDS=20 PAUSE_SECONDS=0.1 \
bash scripts/loadtest/api-loadtest.sh
```
Переменные окружения: `BASE_URL` (по умолчанию `http://localhost:5080`),
`LOGIN`/`PASSWORD`, `DURATION_SECONDS` (15), `PAUSE_SECONDS` (0.2), `CURL_TIMEOUT` (5).
Код возврата: `0` — без ошибок, `1` — ошибка логина/конфигурации, `2` — были не-200.
## Вариант B — k6 (если установлен)
```bash
k6 run scripts/loadtest/deal-loadtest.js
BASE_URL=http://localhost:5080 VUS=10 DURATION=30s \
k6 run scripts/loadtest/deal-loadtest.js
```
Скрипт логинится один раз в `setup()` и раздаёт куку виртуальным пользователям;
пороги проваливают прогон при доле ошибок > 1% или p95 > 500 мс.
## Как поднять API для прогона
- **dev (полный стек)**: `docker compose -f deploy/compose.dev.yml up -d --build`,
затем прогон против `http://localhost:5080`; после — `docker compose -f deploy/compose.dev.yml down`.
- **dev (только core + Postgres)**: см. техдок §13 «Быстрый старт» (Postgres :5433, API :5080).
- Прогон короткий (по умолчанию 15–20 с) — это смоук, а не полноценный бенчмарк.
## Замечания
- В PROD включён rate-limit (`api` — 600/мин на тенанта, `auth` — 10/мин на IP),
поэтому длительные прогоны делайте на dev-стенде или поднимите лимит.
- Метрики перфа сервисов дополнительно смотрите в Prometheus/Grafana (профиль
`observability`): `http_server_request_duration_seconds_*`, RPS/p95 на дашборде
`Deal-Metrics-Overview` (см. техдок §7).
# Нагрузочный смоук-тест API Deal (`scripts/loadtest/`)
Минимальный прогон «есть ли жизнь и как быстро отвечает» по читающим ручкам API
(этап 12, пакет C). Внешних сервисов и записывающих операций нет — только логин
и чтение, БД не мутируется.
## Сценарий
1. `POST /api/auth/login` `{login, password}` — сессия (`deal_session`).
2. В цикле: `GET /api/containers`, затем `GET /api/cards`.
3. Метрики: число запросов, ошибки (не-200), RPS, средняя/p95/max латентность.
Учётные данные по умолчанию — `admin` / `admin` (dev-дефолт bootstrap тенанта).
## Вариант A — bash + curl (зависимостей нет, кроме curl)
```bash
bash scripts/loadtest/api-loadtest.sh
# пример с параметрами
BASE_URL=http://localhost:5080 LOGIN=admin PASSWORD=admin \
DURATION_SECONDS=20 PAUSE_SECONDS=0.1 \
bash scripts/loadtest/api-loadtest.sh
```
Переменные окружения: `BASE_URL` (по умолчанию `http://localhost:5080`),
`LOGIN`/`PASSWORD`, `DURATION_SECONDS` (15), `PAUSE_SECONDS` (0.2), `CURL_TIMEOUT` (5).
Код возврата: `0` — без ошибок, `1` — ошибка логина/конфигурации, `2` — были не-200.
## Вариант B — k6 (если установлен)
```bash
k6 run scripts/loadtest/deal-loadtest.js
BASE_URL=http://localhost:5080 VUS=10 DURATION=30s \
k6 run scripts/loadtest/deal-loadtest.js
```
Скрипт логинится один раз в `setup()` и раздаёт куку виртуальным пользователям;
пороги проваливают прогон при доле ошибок > 1% или p95 > 500 мс.
## Как поднять API для прогона
- **dev (полный стек)**: `docker compose -f deploy/compose.dev.yml up -d --build`,
затем прогон против `http://localhost:5080`; после — `docker compose -f deploy/compose.dev.yml down`.
- **dev (только core + Postgres)**: см. техдок §13 «Быстрый старт» (Postgres :5433, API :5080).
- Прогон короткий (по умолчанию 15–20 с) — это смоук, а не полноценный бенчмарк.
## Замечания
- В PROD включён rate-limit (`api` — 600/мин на тенанта, `auth` — 10/мин на IP),
поэтому длительные прогоны делайте на dev-стенде или поднимите лимит.
- Метрики перфа сервисов дополнительно смотрите в Prometheus/Grafana (профиль
`observability`): `http_server_request_duration_seconds_*`, RPS/p95 на дашборде
`Deal-Metrics-Overview` (см. техдок §7).
+98 -98
View File
@@ -1,98 +1,98 @@
#!/usr/bin/env bash
# Простой нагрузочный смоук-тест API Deal (этап 12, пакет C).
#
# Что делает: логинится (по умолчанию admin/admin), затем в цикле дёргает два
# читающих эндпоинта — GET /api/containers и GET /api/cards — до истечения
# DURATION_SECONDS, замеряя HTTP-код и время ответа. В конце печатает сводку:
# число запросов, ошибки (не 200), RPS, средняя/p95-латентность (сек).
#
# Зависимости: только bash и curl. Ничего не пишет в БД; читающие ручки.
#
# Запуск (из корня репозитория, dev-API на :5080):
# bash scripts/loadtest/api-loadtest.sh
# BASE_URL=http://localhost:5080 LOGIN=admin PASSWORD=admin \
# DURATION_SECONDS=20 PAUSE_SECONDS=0.1 bash scripts/loadtest/api-loadtest.sh
#
# Переменные окружения:
# BASE_URL базовый URL API (по умолчанию http://localhost:5080)
# LOGIN/PASSWORD учётные данные тенанта (по умолчанию admin/admin — dev-дефолт bootstrap)
# DURATION_SECONDS длительность прогона, с (по умолчанию 15)
# PAUSE_SECONDS пауза между итерациями, с (по умолчанию 0.2)
# CURL_TIMEOUT таймаут запроса, с (по умолчанию 5)
#
# Внимание: в PROD включён rate-limit (api-политика — 600/мин на тенанта). Для
# нагрузочного прогона используйте dev-стенд или поднимите лимит.
set -euo pipefail
BASE_URL="${BASE_URL:-http://localhost:5080}"
LOGIN="${LOGIN:-admin}"
PASSWORD="${PASSWORD:-admin}"
DURATION_SECONDS="${DURATION_SECONDS:-15}"
PAUSE_SECONDS="${PAUSE_SECONDS:-0.2}"
CURL_TIMEOUT="${CURL_TIMEOUT:-5}"
command -v curl >/dev/null 2>&1 || { echo "Ошибка: нужен curl" >&2; exit 127; }
tmp_dir="$(mktemp -d)"
cookie_jar="${tmp_dir}/cookies.txt"
lat_file="${tmp_dir}/latencies.txt"
trap 'rm -rf "${tmp_dir}"' EXIT
login_code="$(curl -sS -o "${tmp_dir}/login.json" -w '%{http_code}' \
-c "${cookie_jar}" -H 'Content-Type: application/json' \
-d "{\"login\":\"${LOGIN}\",\"password\":\"${PASSWORD}\"}" \
--max-time "${CURL_TIMEOUT}" "${BASE_URL}/api/auth/login" || true)"
[ -n "${login_code}" ] || login_code="000"
if [ "${login_code}" != "200" ]; then
echo "LOGIN FAILED (HTTP ${login_code}): $(cat "${tmp_dir}/login.json" 2>/dev/null)" >&2
echo "Проверьте BASE_URL=${BASE_URL} и что API поднят." >&2
exit 1
fi
echo "login OK (${LOGIN}) → прогон ${DURATION_SECONDS} с, пауза ${PAUSE_SECONDS} с"
started_at="$(date +%s)"
end_at=$((started_at + DURATION_SECONDS))
total=0
errors=0
while [ "$(date +%s)" -lt "${end_at}" ]; do
for path in /api/containers /api/cards; do
result="$(curl -sS -o /dev/null -w '%{http_code} %{time_total}' \
-b "${cookie_jar}" --max-time "${CURL_TIMEOUT}" "${BASE_URL}${path}" || true)"
[ -n "${result}" ] || result="000 0"
code="${result%% *}"
elapsed="${result##* }"
total=$((total + 1))
if [ "${code}" != "200" ]; then
errors=$((errors + 1))
echo " ! ${path} → HTTP ${code}" >&2
fi
echo "${elapsed}" >> "${lat_file}"
done
sleep "${PAUSE_SECONDS}"
done
elapsed_total=$(( $(date +%s) - started_at ))
[ "${elapsed_total}" -lt 1 ] && elapsed_total=1
if [ "${total}" -eq 0 ]; then
echo "Запросов не сделано (DURATION_SECONDS слишком мал?)" >&2
exit 1
fi
avg="$(awk '{sum += $1} END {printf "%.4f", sum / NR}' "${lat_file}")"
p95_index=$(( (total * 95 + 99) / 100 ))
p95="$(sort -n "${lat_file}" | sed -n "${p95_index}p")"
max="$(sort -n "${lat_file}" | tail -n 1)"
rps="$(awk -v n="${total}" -v d="${elapsed_total}" 'BEGIN {printf "%.2f", n / d}')"
echo "---------------------------------------------"
echo "запросов: ${total}"
echo "ошибок: ${errors}"
echo "длительность: ${elapsed_total} с"
echo "RPS: ${rps}"
echo "латентность: avg=${avg}s p95=${p95}s max=${max}s"
echo "---------------------------------------------"
[ "${errors}" -eq 0 ] || exit 2
#!/usr/bin/env bash
# Простой нагрузочный смоук-тест API Deal (этап 12, пакет C).
#
# Что делает: логинится (по умолчанию admin/admin), затем в цикле дёргает два
# читающих эндпоинта — GET /api/containers и GET /api/cards — до истечения
# DURATION_SECONDS, замеряя HTTP-код и время ответа. В конце печатает сводку:
# число запросов, ошибки (не 200), RPS, средняя/p95-латентность (сек).
#
# Зависимости: только bash и curl. Ничего не пишет в БД; читающие ручки.
#
# Запуск (из корня репозитория, dev-API на :5080):
# bash scripts/loadtest/api-loadtest.sh
# BASE_URL=http://localhost:5080 LOGIN=admin PASSWORD=admin \
# DURATION_SECONDS=20 PAUSE_SECONDS=0.1 bash scripts/loadtest/api-loadtest.sh
#
# Переменные окружения:
# BASE_URL базовый URL API (по умолчанию http://localhost:5080)
# LOGIN/PASSWORD учётные данные тенанта (по умолчанию admin/admin — dev-дефолт bootstrap)
# DURATION_SECONDS длительность прогона, с (по умолчанию 15)
# PAUSE_SECONDS пауза между итерациями, с (по умолчанию 0.2)
# CURL_TIMEOUT таймаут запроса, с (по умолчанию 5)
#
# Внимание: в PROD включён rate-limit (api-политика — 600/мин на тенанта). Для
# нагрузочного прогона используйте dev-стенд или поднимите лимит.
set -euo pipefail
BASE_URL="${BASE_URL:-http://localhost:5080}"
LOGIN="${LOGIN:-admin}"
PASSWORD="${PASSWORD:-admin}"
DURATION_SECONDS="${DURATION_SECONDS:-15}"
PAUSE_SECONDS="${PAUSE_SECONDS:-0.2}"
CURL_TIMEOUT="${CURL_TIMEOUT:-5}"
command -v curl >/dev/null 2>&1 || { echo "Ошибка: нужен curl" >&2; exit 127; }
tmp_dir="$(mktemp -d)"
cookie_jar="${tmp_dir}/cookies.txt"
lat_file="${tmp_dir}/latencies.txt"
trap 'rm -rf "${tmp_dir}"' EXIT
login_code="$(curl -sS -o "${tmp_dir}/login.json" -w '%{http_code}' \
-c "${cookie_jar}" -H 'Content-Type: application/json' \
-d "{\"login\":\"${LOGIN}\",\"password\":\"${PASSWORD}\"}" \
--max-time "${CURL_TIMEOUT}" "${BASE_URL}/api/auth/login" || true)"
[ -n "${login_code}" ] || login_code="000"
if [ "${login_code}" != "200" ]; then
echo "LOGIN FAILED (HTTP ${login_code}): $(cat "${tmp_dir}/login.json" 2>/dev/null)" >&2
echo "Проверьте BASE_URL=${BASE_URL} и что API поднят." >&2
exit 1
fi
echo "login OK (${LOGIN}) → прогон ${DURATION_SECONDS} с, пауза ${PAUSE_SECONDS} с"
started_at="$(date +%s)"
end_at=$((started_at + DURATION_SECONDS))
total=0
errors=0
while [ "$(date +%s)" -lt "${end_at}" ]; do
for path in /api/containers /api/cards; do
result="$(curl -sS -o /dev/null -w '%{http_code} %{time_total}' \
-b "${cookie_jar}" --max-time "${CURL_TIMEOUT}" "${BASE_URL}${path}" || true)"
[ -n "${result}" ] || result="000 0"
code="${result%% *}"
elapsed="${result##* }"
total=$((total + 1))
if [ "${code}" != "200" ]; then
errors=$((errors + 1))
echo " ! ${path} → HTTP ${code}" >&2
fi
echo "${elapsed}" >> "${lat_file}"
done
sleep "${PAUSE_SECONDS}"
done
elapsed_total=$(( $(date +%s) - started_at ))
[ "${elapsed_total}" -lt 1 ] && elapsed_total=1
if [ "${total}" -eq 0 ]; then
echo "Запросов не сделано (DURATION_SECONDS слишком мал?)" >&2
exit 1
fi
avg="$(awk '{sum += $1} END {printf "%.4f", sum / NR}' "${lat_file}")"
p95_index=$(( (total * 95 + 99) / 100 ))
p95="$(sort -n "${lat_file}" | sed -n "${p95_index}p")"
max="$(sort -n "${lat_file}" | tail -n 1)"
rps="$(awk -v n="${total}" -v d="${elapsed_total}" 'BEGIN {printf "%.2f", n / d}')"
echo "---------------------------------------------"
echo "запросов: ${total}"
echo "ошибок: ${errors}"
echo "длительность: ${elapsed_total} с"
echo "RPS: ${rps}"
echo "латентность: avg=${avg}s p95=${p95}s max=${max}s"
echo "---------------------------------------------"
[ "${errors}" -eq 0 ] || exit 2
+56 -56
View File
@@ -1,56 +1,56 @@
// Нагрузочный смоук-тест API Deal на k6 (этап 12, пакет C) — альтернатива
// scripts/loadtest/api-loadtest.sh, если в системе есть k6.
//
// Логин — в setup() (один раз), затем виртуальные пользователи в цикле читают
// GET /api/containers и GET /api/cards. Пороги (thresholds) проваливают прогон
// при доле ошибок > 1% или p95 > 500 мс.
//
// Запуск (API dev на :5080):
// k6 run scripts/loadtest/deal-loadtest.js
// BASE_URL=http://localhost:5080 VUS=10 DURATION=30s k6 run scripts/loadtest/deal-loadtest.js
//
// Переменные окружения: BASE_URL, LOGIN, PASSWORD, VUS, DURATION.
// Внимание: в PROD включён rate-limit (api — 600/мин на тенанта) — гоняйте на dev.
import http from 'k6/http';
import { check, sleep } from 'k6';
const BASE_URL = __ENV.BASE_URL || 'http://localhost:5080';
const LOGIN = __ENV.LOGIN || 'admin';
const PASSWORD = __ENV.PASSWORD || 'admin';
export const options = {
vus: __ENV.VUS ? Number(__ENV.VUS) : 5,
duration: __ENV.DURATION || '20s',
thresholds: {
http_req_failed: ['rate<0.01'],
http_req_duration: ['p(95)<500'],
},
};
export function setup() {
const res = http.post(
`${BASE_URL}/api/auth/login`,
JSON.stringify({ login: LOGIN, password: PASSWORD }),
{ headers: { 'Content-Type': 'application/json' } }
);
if (res.status !== 200) {
throw new Error(`login failed: HTTP ${res.status}${res.body}`);
}
const cookies = res.cookies['deal_session'];
const session = cookies && cookies.length ? cookies[0].value : '';
if (!session) {
throw new Error('login succeeded but deal_session cookie is missing');
}
return { session };
}
export default function (data) {
const params = { headers: { Cookie: `deal_session=${data.session}` } };
const paths = ['/api/containers', '/api/cards'];
for (const path of paths) {
const res = http.get(`${BASE_URL}${path}`, params);
check(res, { [`${path} → 200`]: (r) => r.status === 200 });
}
sleep(0.2);
}
// Нагрузочный смоук-тест API Deal на k6 (этап 12, пакет C) — альтернатива
// scripts/loadtest/api-loadtest.sh, если в системе есть k6.
//
// Логин — в setup() (один раз), затем виртуальные пользователи в цикле читают
// GET /api/containers и GET /api/cards. Пороги (thresholds) проваливают прогон
// при доле ошибок > 1% или p95 > 500 мс.
//
// Запуск (API dev на :5080):
// k6 run scripts/loadtest/deal-loadtest.js
// BASE_URL=http://localhost:5080 VUS=10 DURATION=30s k6 run scripts/loadtest/deal-loadtest.js
//
// Переменные окружения: BASE_URL, LOGIN, PASSWORD, VUS, DURATION.
// Внимание: в PROD включён rate-limit (api — 600/мин на тенанта) — гоняйте на dev.
import http from 'k6/http';
import { check, sleep } from 'k6';
const BASE_URL = __ENV.BASE_URL || 'http://localhost:5080';
const LOGIN = __ENV.LOGIN || 'admin';
const PASSWORD = __ENV.PASSWORD || 'admin';
export const options = {
vus: __ENV.VUS ? Number(__ENV.VUS) : 5,
duration: __ENV.DURATION || '20s',
thresholds: {
http_req_failed: ['rate<0.01'],
http_req_duration: ['p(95)<500'],
},
};
export function setup() {
const res = http.post(
`${BASE_URL}/api/auth/login`,
JSON.stringify({ login: LOGIN, password: PASSWORD }),
{ headers: { 'Content-Type': 'application/json' } }
);
if (res.status !== 200) {
throw new Error(`login failed: HTTP ${res.status}${res.body}`);
}
const cookies = res.cookies['deal_session'];
const session = cookies && cookies.length ? cookies[0].value : '';
if (!session) {
throw new Error('login succeeded but deal_session cookie is missing');
}
return { session };
}
export default function (data) {
const params = { headers: { Cookie: `deal_session=${data.session}` } };
const paths = ['/api/containers', '/api/cards'];
for (const path of paths) {
const res = http.get(`${BASE_URL}${path}`, params);
check(res, { [`${path} → 200`]: (r) => r.status === 200 });
}
sleep(0.2);
}