Инициализировать репозиторий «Дейл»

Первый коммит: модульный монолит ядра (.NET 10) и gRPC-сервисы
ai/ml/telegram, фронтенд Vue 3/Vite/Tailwind, документация (ТЗ,
инструкция пользователя, техдокументация, код-стайл), бэклог,
скрипты развёртывания и архив прототипа LeadRadar.
This commit is contained in:
Rustam Khalimov
2026-09-11 02:50:17 +03:00
commit 9e07568ddd
1402 changed files with 177470 additions and 0 deletions
+98
View File
@@ -0,0 +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