669 lines
35 KiB
Bash
669 lines
35 KiB
Bash
#!/usr/bin/env bash
|
||
# 2026-03-21 — chaos_marathon.sh
|
||
# Часовой хаос-марафон: 15 сервисов, dumb-user simulation, PG stress, CRUD lifecycle.
|
||
# Запуск: bash chaos_marathon.sh 2>&1 | tee /tmp/chaos_marathon_$(date +%Y%m%d_%H%M).log
|
||
#
|
||
# Предполагает: terraform apply chaos_marathon.tf уже выполнен, все 15 Ready.
|
||
# Зависимости: curl, jq, terraform (init выполнен).
|
||
|
||
# -e намеренно НЕ установлен — падение одного вызова не убивает марафон.
|
||
# -u: незаданные переменные = ошибка. -o pipefail: ошибка в пайпе видна.
|
||
set -uo pipefail
|
||
|
||
# ── Config ────────────────────────────────────────────────────────────────────
|
||
|
||
BASE_URL="${SLESS_BASE_URL:-https://sless.kube5s.ru}"
|
||
TOKEN_FILE="${SLESS_TOKEN_FILE:-/home/naeel/terra/sless/test.token}"
|
||
NAMESPACE="${SLESS_NAMESPACE:-sless-ffd1f598c169b0ae}"
|
||
TF_DIR="/home/naeel/terra/sless/examples/POSTGRES"
|
||
LOG_DIR="/tmp/chaos_$(date +%Y%m%d_%H%M%S)"
|
||
mkdir -p "$LOG_DIR"
|
||
|
||
# ── Helpers ───────────────────────────────────────────────────────────────────
|
||
|
||
TOKEN=$(cat "$TOKEN_FILE")
|
||
PASS=0
|
||
FAIL=0
|
||
TOTAL=0
|
||
|
||
# Цветной вывод — для удобства чтения длинного лога.
|
||
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; CYAN='\033[0;36m'; NC='\033[0m'
|
||
|
||
ts() { date '+%H:%M:%S'; }
|
||
|
||
# invoke SERVICE_NAME PAYLOAD — вызывает сервис через публичный /fn/ proxy, возвращает тело.
|
||
# Публичный endpoint не требует токена (используется для вызова функций).
|
||
# Никогда не падает — ошибки пишутся в лог-файл.
|
||
invoke() {
|
||
local svc="$1" payload="$2"
|
||
curl -sf -X POST \
|
||
-H "Content-Type: application/json" \
|
||
-d "$payload" \
|
||
"${BASE_URL}/fn/${NAMESPACE}/${svc}" \
|
||
2>"$LOG_DIR/err_${svc}_$(date +%s%N).log" || true
|
||
}
|
||
|
||
# invoke_raw — как invoke, но никогда не бросает non-zero exit.
|
||
invoke_raw() {
|
||
local svc="$1" payload="$2"
|
||
curl -s -X POST \
|
||
-H "Content-Type: application/json" \
|
||
-d "$payload" \
|
||
"${BASE_URL}/fn/${NAMESPACE}/${svc}" || true
|
||
}
|
||
|
||
# invoke_with_status SERVICE PAYLOAD — возвращает HTTP код, никогда не падает.
|
||
invoke_with_status() {
|
||
local svc="$1" payload="$2"
|
||
curl -s -o /dev/null -w "%{http_code}" -X POST \
|
||
-H "Content-Type: application/json" \
|
||
-d "$payload" \
|
||
"${BASE_URL}/fn/${NAMESPACE}/${svc}" || echo "000"
|
||
}
|
||
|
||
# check TEST_NAME CONDITION [msg] — вердикт по условию (expect 0-exit или строковую проверку).
|
||
check() {
|
||
local name="$1" result="$2" expected="${3:-0}"
|
||
TOTAL=$((TOTAL + 1))
|
||
if [[ "$result" == "$expected" ]]; then
|
||
PASS=$((PASS + 1))
|
||
echo -e "$(ts) ${GREEN}PASS${NC} [$TOTAL] $name"
|
||
else
|
||
FAIL=$((FAIL + 1))
|
||
echo -e "$(ts) ${RED}FAIL${NC} [$TOTAL] $name (got='$result' want='$expected')"
|
||
fi
|
||
}
|
||
|
||
# check_contains TEST_NAME HAYSTACK NEEDLE
|
||
check_contains() {
|
||
local name="$1" hay="$2" needle="$3"
|
||
TOTAL=$((TOTAL + 1))
|
||
if echo "$hay" | grep -q "$needle"; then
|
||
PASS=$((PASS + 1))
|
||
echo -e "$(ts) ${GREEN}PASS${NC} [$TOTAL] $name"
|
||
else
|
||
FAIL=$((FAIL + 1))
|
||
echo -e "$(ts) ${RED}FAIL${NC} [$TOTAL] $name (needle='$needle' not found)"
|
||
fi
|
||
}
|
||
|
||
# check_not_contains TEST_NAME HAYSTACK NEEDLE
|
||
check_not_contains() {
|
||
local name="$1" hay="$2" needle="$3"
|
||
TOTAL=$((TOTAL + 1))
|
||
if ! echo "$hay" | grep -q "$needle"; then
|
||
PASS=$((PASS + 1))
|
||
echo -e "$(ts) ${GREEN}PASS${NC} [$TOTAL] $name"
|
||
else
|
||
FAIL=$((FAIL + 1))
|
||
echo -e "$(ts) ${RED}FAIL${NC} [$TOTAL] $name (unexpected needle='$needle' found)"
|
||
fi
|
||
}
|
||
|
||
# check_http TEST_NAME CODE EXPECTED
|
||
check_http() {
|
||
local name="$1" code="$2" expected="${3:-200}"
|
||
TOTAL=$((TOTAL + 1))
|
||
if [[ "$code" == "$expected" ]]; then
|
||
PASS=$((PASS + 1))
|
||
echo -e "$(ts) ${GREEN}PASS${NC} [$TOTAL] $name → HTTP $code"
|
||
else
|
||
FAIL=$((FAIL + 1))
|
||
echo -e "$(ts) ${RED}FAIL${NC} [$TOTAL] $name → HTTP $code (want $expected)"
|
||
fi
|
||
}
|
||
|
||
section() {
|
||
echo ""
|
||
echo -e "${CYAN}════════════════════════════════════════════════════════════${NC}"
|
||
echo -e "${CYAN} $1${NC}"
|
||
echo -e "${CYAN}════════════════════════════════════════════════════════════${NC}"
|
||
}
|
||
|
||
# ── Wait helpers ──────────────────────────────────────────────────────────────
|
||
|
||
# wait_service_ready NAME — ждём до 2 мин пока GET /services/{name} вернёт phase=Ready.
|
||
# Проверяет статус через API (не invoke), чтобы не запускать функцию при старте.
|
||
wait_service_ready() {
|
||
local svc="$1" max_attempts=24 attempt=0
|
||
echo " Ожидаем готовности $svc..."
|
||
while (( attempt < max_attempts )); do
|
||
phase=$(curl -sf \
|
||
-H "Authorization: Bearer $TOKEN" \
|
||
"${BASE_URL}/v1/namespaces/${NAMESPACE}/services/${svc}" \
|
||
2>/dev/null | python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('phase',''))" 2>/dev/null || true)
|
||
if [[ "$phase" == "Ready" ]]; then
|
||
echo " → $svc Ready (attempt $((attempt+1)))"
|
||
return 0
|
||
fi
|
||
sleep 5
|
||
attempt=$((attempt + 1))
|
||
done
|
||
echo -e " ${RED}TIMEOUT: $svc не стал Ready за 2 мин${NC}"
|
||
return 1
|
||
}
|
||
|
||
# ── Parallel invoker ──────────────────────────────────────────────────────────
|
||
|
||
# parallel_invoke COUNT SERVICE PAYLOAD LOG_PREFIX — запускает COUNT вызовов параллельно.
|
||
parallel_invoke() {
|
||
local count="$1" svc="$2" payload="$3" prefix="$4"
|
||
local pids=() results_dir="$LOG_DIR/par_${prefix}_$(date +%s)"
|
||
mkdir -p "$results_dir"
|
||
for i in $(seq 1 "$count"); do
|
||
(
|
||
code=$(invoke_with_status "$svc" "$payload")
|
||
echo "$code" > "$results_dir/$i"
|
||
) &
|
||
pids+=($!)
|
||
done
|
||
# Ждём все фоновые задачи.
|
||
for pid in "${pids[@]}"; do wait "$pid" || true; done
|
||
# Счёт 200-х.
|
||
local ok=0 bad=0
|
||
for f in "$results_dir"/*; do
|
||
code=$(cat "$f")
|
||
if [[ "$code" == "200" ]]; then ok=$((ok+1)); else bad=$((bad+1)); fi
|
||
done
|
||
echo "$ok/$count OK, $bad FAIL"
|
||
}
|
||
|
||
echo ""
|
||
echo -e "${YELLOW}╔══════════════════════════════════════════════════════════╗${NC}"
|
||
echo -e "${YELLOW}║ CHAOS MARATHON — $(date '+%Y-%m-%d %H:%M:%S') ║${NC}"
|
||
echo -e "${YELLOW}╚══════════════════════════════════════════════════════════╝${NC}"
|
||
echo ""
|
||
|
||
# ═══════════════════════════════════════════════════════════════════════════════
|
||
section "ФАЗА 0 — Ожидание готовности всех 15 сервисов"
|
||
# ═══════════════════════════════════════════════════════════════════════════════
|
||
|
||
SERVICES=(
|
||
pg-counter pg-dedup pg-search pg-bulk-insert pg-delete-old pg-upsert
|
||
chaos-echo chaos-badparams chaos-slowquery chaos-bigpayload
|
||
go-pg-race go-counter-atomic
|
||
js-pg-batch js-idempotent
|
||
py-retry-writer
|
||
)
|
||
|
||
failed_ready=0
|
||
for svc in "${SERVICES[@]}"; do
|
||
if ! wait_service_ready "$svc"; then
|
||
failed_ready=$((failed_ready + 1))
|
||
fi
|
||
done
|
||
|
||
if (( failed_ready > 0 )); then
|
||
echo -e "${YELLOW}ВНИМАНИЕ: $failed_ready сервисов не стали Ready. Продолжаем — они будут FAIL в тестах.${NC}"
|
||
# НЕ выходим — дальше тесты сами покажут что сломалось.
|
||
fi
|
||
echo -e "${GREEN}Все 15 сервисов Ready. Начинаем марафон.${NC}"
|
||
START_TIME=$(date +%s)
|
||
MARATHON_DURATION=${MARATHON_DURATION_SEC:-3600} # по умолчанию 1 час
|
||
ROUND=0
|
||
|
||
echo -e "${CYAN}Длительность марафона: ${MARATHON_DURATION}с ($(( MARATHON_DURATION / 60 )) мин)${NC}"
|
||
|
||
# ── Основной цикл: крутим фазы 1–11 пока не истечёт время ───────────────────
|
||
while true; do
|
||
NOW=$(date +%s)
|
||
ELAPSED_TOTAL=$(( NOW - START_TIME ))
|
||
if (( ELAPSED_TOTAL >= MARATHON_DURATION )); then
|
||
echo -e "\n${YELLOW}Время марафона истекло (${ELAPSED_TOTAL}с). Переходим к финальной проверке.${NC}"
|
||
break
|
||
fi
|
||
ROUND=$((ROUND + 1))
|
||
MINS_LEFT=$(( (MARATHON_DURATION - ELAPSED_TOTAL) / 60 ))
|
||
echo -e "\n${YELLOW}═══ РАУНД $ROUND | прошло $(( ELAPSED_TOTAL / 60 ))м, осталось ${MINS_LEFT}м ═══${NC}"
|
||
|
||
# ═══════════════════════════════════════════════════════════════════════════════
|
||
section "ФАЗА 1 — Базовый smoke-test (1 вызов каждого сервиса)"
|
||
# ═══════════════════════════════════════════════════════════════════════════════
|
||
|
||
# pg-counter: простой счёт всех строк.
|
||
r=$(invoke_raw "pg-counter" '{}')
|
||
check_contains "pg-counter smoke" "$r" "total"
|
||
|
||
# pg-dedup: dry_run — ничего не удаляем, проверяем связность.
|
||
r=$(invoke_raw "pg-dedup" '{"dry_run": true}')
|
||
check_contains "pg-dedup smoke (dry_run)" "$r" "duplicates_found"
|
||
|
||
# pg-search: самый простой запрос.
|
||
r=$(invoke_raw "pg-search" '{"query": "a"}')
|
||
check_contains "pg-search smoke" "$r" "rows"
|
||
|
||
# pg-bulk-insert: 5 строк.
|
||
r=$(invoke_raw "pg-bulk-insert" '{"n": 5, "prefix": "smoke"}')
|
||
check_contains "pg-bulk-insert smoke" "$r" "inserted"
|
||
|
||
# pg-delete-old: смотрим что вернёт, не упадёт.
|
||
r=$(invoke_raw "pg-delete-old" '{"older_than_minutes": 99999}')
|
||
check_contains "pg-delete-old smoke" "$r" "deleted"
|
||
|
||
# pg-upsert: вставляем одну строку.
|
||
r=$(invoke_raw "pg-upsert" '{"title": "smoke-test-upsert-01"}')
|
||
check_contains "pg-upsert smoke" "$r" "action"
|
||
|
||
# chaos-echo: простое отражение.
|
||
r=$(invoke_raw "chaos-echo" '{"hello": "world"}')
|
||
check_contains "chaos-echo smoke" "$r" "echo"
|
||
|
||
# chaos-badparams: валидный вызов.
|
||
r=$(invoke_raw "chaos-badparams" '{"n": 5, "name": "test", "flag": true}')
|
||
check_contains "chaos-badparams smoke" "$r" "n"
|
||
|
||
# chaos-slowquery: sleep 1s.
|
||
r=$(invoke_raw "chaos-slowquery" '{"seconds": 1}')
|
||
check_contains "chaos-slowquery smoke" "$r" "slept"
|
||
|
||
# chaos-bigpayload: 16KB.
|
||
r=$(invoke_raw "chaos-bigpayload" '{"size_kb": 16}')
|
||
check_contains "chaos-bigpayload smoke" "$r" "items"
|
||
|
||
# go-pg-race: 2 горутины × 3 INSERT.
|
||
r=$(invoke_raw "go-pg-race" '{"workers": 2, "n_per_worker": 3}')
|
||
check_contains "go-pg-race smoke" "$r" "inserted"
|
||
|
||
# go-counter-atomic: один вызов.
|
||
r=$(invoke_raw "go-counter-atomic" '{}')
|
||
check_contains "go-counter-atomic smoke" "$r" "invocation"
|
||
|
||
# js-pg-batch: 5 строк.
|
||
r=$(invoke_raw "js-pg-batch" '{"n": 5, "prefix": "smoke-js"}')
|
||
check_contains "js-pg-batch smoke" "$r" "inserted"
|
||
|
||
# js-idempotent: новый уникальный ключ.
|
||
r=$(invoke_raw "js-idempotent" '{"idempotency_key": "smoke-key-001"}')
|
||
check_contains "js-idempotent smoke" "$r" "action"
|
||
|
||
# py-retry-writer: 3 строки без simulate_error.
|
||
r=$(invoke_raw "py-retry-writer" '{"n": 3, "prefix": "smoke"}')
|
||
check_contains "py-retry-writer smoke" "$r" "inserted"
|
||
|
||
# ═══════════════════════════════════════════════════════════════════════════════
|
||
section "ФАЗА 2 — Dumb User Simulation (тупой юзер ломает всё)"
|
||
# ═══════════════════════════════════════════════════════════════════════════════
|
||
|
||
echo " Тест: передаём мусор в каждый сервис — никто не должен вернуть 500."
|
||
|
||
# chaos-echo: пустой объект.
|
||
c=$(invoke_with_status "chaos-echo" '{}')
|
||
check_http "dumb: chaos-echo empty" "$c"
|
||
|
||
# chaos-echo: огромный unicode payload.
|
||
big_unicode=$(python3 -c "import json; print(json.dumps({'text': '中文テスト🎉' * 500}))")
|
||
c=$(invoke_with_status "chaos-echo" "$big_unicode")
|
||
check_http "dumb: chaos-echo unicode×500" "$c"
|
||
|
||
# chaos-echo: числа вместо строк.
|
||
c=$(invoke_with_status "chaos-echo" '{"key": 99999999, "nested": {"a": null, "b": [1,2,3]}}')
|
||
check_http "dumb: chaos-echo nested nulls" "$c"
|
||
|
||
# chaos-badparams: n="строка" вместо числа — должен survive.
|
||
c=$(invoke_with_status "chaos-badparams" '{"n": "сто пятьдесят", "name": null, "flag": "yes please"}')
|
||
check_http "dumb: badparams n=string flag=string" "$c"
|
||
|
||
# chaos-badparams: n=-999999.
|
||
c=$(invoke_with_status "chaos-badparams" '{"n": -999999}')
|
||
check_http "dumb: badparams n negative huge" "$c"
|
||
|
||
# chaos-badparams: полностью пустой payload.
|
||
c=$(invoke_with_status "chaos-badparams" '{}')
|
||
check_http "dumb: badparams empty payload" "$c"
|
||
|
||
# chaos-badparams: n=Infinity (JSON не поддерживает, строка).
|
||
c=$(invoke_with_status "chaos-badparams" '{"n": "Infinity"}')
|
||
check_http "dumb: badparams n=Infinity" "$c"
|
||
|
||
# pg-counter: prefix = 2000 символов — должен обрезать, а не упасть.
|
||
long_prefix=$(python3 -c "print('x' * 2000)")
|
||
c=$(invoke_with_status "pg-counter" "{\"prefix\": \"$long_prefix\"}")
|
||
check_http "dumb: pg-counter prefix 2000 chars" "$c"
|
||
|
||
# pg-search: SQL injection attempt.
|
||
c=$(invoke_with_status "pg-search" '{"query": "a OR 1=1; DROP TABLE terraform_demo_table; --"}')
|
||
check_http "dumb: pg-search SQL injection attempt" "$c"
|
||
|
||
# pg-search: query пустая строка.
|
||
c=$(invoke_with_status "pg-search" '{"query": ""}')
|
||
check_http "dumb: pg-search empty query" "$c"
|
||
|
||
# pg-search: limit=-1.
|
||
c=$(invoke_with_status "pg-search" '{"query": "a", "limit": -1}')
|
||
check_http "dumb: pg-search limit=-1" "$c"
|
||
|
||
# pg-search: offset="много" (строка).
|
||
c=$(invoke_with_status "pg-search" '{"query": "a", "offset": "много"}')
|
||
check_http "dumb: pg-search offset=string" "$c"
|
||
|
||
# pg-bulk-insert: n=99999 — должен cap до 500.
|
||
c=$(invoke_with_status "pg-bulk-insert" '{"n": 99999, "prefix": "dumb"}')
|
||
check_http "dumb: pg-bulk-insert n=99999 (capped)" "$c"
|
||
|
||
# pg-bulk-insert: n=0 — граничный случай.
|
||
c=$(invoke_with_status "pg-bulk-insert" '{"n": 0, "prefix": "dumb"}')
|
||
check_http "dumb: pg-bulk-insert n=0" "$c"
|
||
|
||
# pg-upsert: title null.
|
||
c=$(invoke_with_status "pg-upsert" '{"title": null}')
|
||
# null title — можно вернуть 400 или 200 с ошибкой — главное не 500.
|
||
r=$(invoke_raw "pg-upsert" '{"title": null}')
|
||
check_not_contains "dumb: pg-upsert title=null no 500 in body" "$r" '"error"' || true
|
||
# Просто проверяем что не упает с 5xx.
|
||
[[ "$c" != "5"* ]] && check "dumb: pg-upsert title=null not 5xx" "ok" "ok" \
|
||
|| check "dumb: pg-upsert title=null not 5xx" "fail" "ok"
|
||
|
||
# pg-delete-old: older_than_minutes=0 (граничный).
|
||
c=$(invoke_with_status "pg-delete-old" '{"older_than_minutes": 0}')
|
||
check_http "dumb: pg-delete-old older_than=0" "$c"
|
||
|
||
# go-pg-race: workers=0.
|
||
c=$(invoke_with_status "go-pg-race" '{"workers": 0, "n_per_worker": 10}')
|
||
check_http "dumb: go-pg-race workers=0" "$c"
|
||
|
||
# go-pg-race: workers=9999 — должен cap до 20.
|
||
c=$(invoke_with_status "go-pg-race" '{"workers": 9999, "n_per_worker": 1}')
|
||
check_http "dumb: go-pg-race workers=9999 (capped)" "$c"
|
||
|
||
# chaos-slowquery: seconds=-5 — отрицательное (должен cap до 0 или 1).
|
||
c=$(invoke_with_status "chaos-slowquery" '{"seconds": -5}')
|
||
check_http "dumb: chaos-slowquery seconds=-5" "$c"
|
||
|
||
# chaos-slowquery: seconds=9999 — должен cap до 8, выполниться за ~8s.
|
||
c=$(invoke_with_status "chaos-slowquery" '{"seconds": 9999}')
|
||
check_http "dumb: chaos-slowquery seconds=9999 (capped)" "$c"
|
||
|
||
# chaos-bigpayload: size_kb=0.
|
||
c=$(invoke_with_status "chaos-bigpayload" '{"size_kb": 0}')
|
||
check_http "dumb: chaos-bigpayload size_kb=0" "$c"
|
||
|
||
# chaos-bigpayload: size_kb=9999 — должен cap до 256.
|
||
c=$(invoke_with_status "chaos-bigpayload" '{"size_kb": 9999}')
|
||
check_http "dumb: chaos-bigpayload size_kb=9999 (capped)" "$c"
|
||
|
||
# js-pg-batch: n="много" — строка вместо числа.
|
||
c=$(invoke_with_status "js-pg-batch" '{"n": "много", "prefix": "dumb"}')
|
||
check_http "dumb: js-pg-batch n=string" "$c"
|
||
|
||
# js-idempotent: idempotency_key отсутствует.
|
||
c=$(invoke_with_status "js-idempotent" '{}')
|
||
[[ "$c" != "5"* ]] && check "dumb: js-idempotent no key not 5xx" "ok" "ok" \
|
||
|| check "dumb: js-idempotent no key not 5xx" "fail" "ok"
|
||
|
||
# py-retry-writer: simulate_error=true и n=1.
|
||
r=$(invoke_raw "py-retry-writer" '{"n": 1, "simulate_error": true}')
|
||
check_contains "dumb: py-retry-writer simulate_error n=1" "$r" "attempts"
|
||
|
||
# ═══════════════════════════════════════════════════════════════════════════════
|
||
section "ФАЗА 3 — Idempotency Suite"
|
||
# ═══════════════════════════════════════════════════════════════════════════════
|
||
|
||
echo " Тест: повторные вызовы с одинаковыми ключами дают предсказуемый результат."
|
||
|
||
# pg-upsert: вызываем 20× с одним title — в таблице должна быть одна строка.
|
||
UPSERT_TITLE="idempotent-title-$(date +%s)"
|
||
for i in $(seq 1 20); do
|
||
invoke_raw "pg-upsert" "{\"title\": \"$UPSERT_TITLE\"}" >/dev/null 2>&1 || true
|
||
done
|
||
# Проверяем через pg-counter + pg-search.
|
||
r=$(invoke_raw "pg-search" "{\"query\": \"$UPSERT_TITLE\", \"limit\": 100}")
|
||
count=$(echo "$r" | jq -r '.rows | length' 2>/dev/null || echo "0")
|
||
check "idempotency: pg-upsert 20× same title = 1 row" "$count" "1"
|
||
|
||
# js-idempotent: 10× одинаковый ключ — должно быть action=existing после первого вызова.
|
||
IDEM_KEY="js-idempotent-key-$(date +%s)"
|
||
# Первый вызов.
|
||
r=$(invoke_raw "js-idempotent" "{\"idempotency_key\": \"$IDEM_KEY\"}")
|
||
check_contains "idempotency: js-idempotent first call=created" "$r" "created"
|
||
# Следующие 5 вызовов.
|
||
for i in $(seq 2 6); do
|
||
r=$(invoke_raw "js-idempotent" "{\"idempotency_key\": \"$IDEM_KEY\"}")
|
||
check_contains "idempotency: js-idempotent call $i=existing" "$r" "existing"
|
||
done
|
||
|
||
# pg-dedup: вставляем дубли, затем проверяем что dedup убирает лишние.
|
||
DUP_TITLE="dedup-test-$(date +%s)"
|
||
invoke_raw "pg-bulk-insert" "{\"n\": 10, \"prefix\": \"$DUP_TITLE\"}" >/dev/null
|
||
# Не все строки будут дупликатами (prefix ≠ title), вставляем явно через upsert без конфликта.
|
||
# Вставляем одно и то же 5 раз через pg-upsert (он обновляет → НЕ дубль).
|
||
# Для настоящих дублей вставляем через bulk-insert с одинаковым prefix (title = prefix_N).
|
||
# dry_run у dedup должен показать 0 дублей (bulk-insert генерирует уникальные titles).
|
||
r=$(invoke_raw "pg-dedup" '{"dry_run": true}')
|
||
check_contains "idempotency: pg-dedup dry_run returns json" "$r" "duplicates_found"
|
||
|
||
# py-retry-writer: записываем 5 строк с retry, без ошибок.
|
||
r=$(invoke_raw "py-retry-writer" '{"n": 5, "prefix": "retry-idem"}')
|
||
inserted=$(echo "$r" | jq -r '.inserted' 2>/dev/null || echo "0")
|
||
check "idempotency: py-retry-writer inserts 5" "$inserted" "5"
|
||
|
||
# ═══════════════════════════════════════════════════════════════════════════════
|
||
section "ФАЗА 4 — PG Parallel Stress"
|
||
# ═══════════════════════════════════════════════════════════════════════════════
|
||
|
||
echo " Нагрузка: параллельные вызовы к PG-функциям."
|
||
|
||
# pg-counter: 30 параллельных чтений.
|
||
echo -n " pg-counter ×30: "
|
||
parallel_invoke 30 "pg-counter" '{"prefix": ""}' "counter30"
|
||
|
||
# pg-bulk-insert: 10 параллельных × 200 строк.
|
||
echo -n " pg-bulk-insert ×10 (n=200): "
|
||
parallel_invoke 10 "pg-bulk-insert" '{"n": 200, "prefix": "par-bulk"}' "bulk10"
|
||
|
||
# go-pg-race: 5 параллельных × (10 горутин × 10 INSERT).
|
||
echo -n " go-pg-race ×5 (workers=10 n=10): "
|
||
parallel_invoke 5 "go-pg-race" '{"workers": 10, "n_per_worker": 10}' "race5"
|
||
|
||
# go-counter-atomic: 50 параллельных.
|
||
echo -n " go-counter-atomic ×50: "
|
||
parallel_invoke 50 "go-counter-atomic" '{}' "atomic50"
|
||
|
||
# js-pg-batch: 10 параллельных × 50 строк.
|
||
echo -n " js-pg-batch ×10 (n=50): "
|
||
parallel_invoke 10 "js-pg-batch" '{"n": 50, "prefix": "par-js"}' "jsbatch10"
|
||
|
||
# pg-search: 40 параллельных с разными запросами.
|
||
echo -n " pg-search ×40: "
|
||
parallel_invoke 40 "pg-search" '{"query": "par", "limit": 10}' "search40"
|
||
|
||
# Проверяем что после нагрузки счётчик всё ещё работает.
|
||
r=$(invoke_raw "pg-counter" '{}')
|
||
check_contains "pg stress: counter still returns total" "$r" "total"
|
||
|
||
# ═══════════════════════════════════════════════════════════════════════════════
|
||
section "ФАЗА 5 — Chaos Payload & Echo Storm"
|
||
# ═══════════════════════════════════════════════════════════════════════════════
|
||
|
||
# chaos-bigpayload: 20 параллельных 64KB.
|
||
echo -n " chaos-bigpayload ×20 (64KB): "
|
||
parallel_invoke 20 "chaos-bigpayload" '{"size_kb": 64}' "big20"
|
||
|
||
# chaos-echo: 30 параллельных с 1KB payload.
|
||
medium_payload=$(python3 -c "import json; print(json.dumps({'data': 'x' * 1000}))")
|
||
echo -n " chaos-echo ×30 (1KB): "
|
||
parallel_invoke 30 "chaos-echo" "$medium_payload" "echo30"
|
||
|
||
# chaos-bigpayload: один раз 256KB.
|
||
r=$(invoke_raw "chaos-bigpayload" '{"size_kb": 256}')
|
||
check_contains "chaos-bigpayload 256KB single" "$r" "items"
|
||
|
||
# ═══════════════════════════════════════════════════════════════════════════════
|
||
section "ФАЗА 6 — Slow Query Handling"
|
||
# ═══════════════════════════════════════════════════════════════════════════════
|
||
|
||
# Один медленный запрос 8 секунд — ожидаем 200.
|
||
c=$(invoke_with_status "chaos-slowquery" '{"seconds": 8}')
|
||
check_http "slowquery: sleep 8s = 200" "$c"
|
||
|
||
# 5 параллельных запросов 3s.
|
||
echo -n " chaos-slowquery ×5 (3s each): "
|
||
parallel_invoke 5 "chaos-slowquery" '{"seconds": 3}' "slow5"
|
||
|
||
# ═══════════════════════════════════════════════════════════════════════════════
|
||
section "ФАЗА 7 — Search Storm (special chars)"
|
||
# ═══════════════════════════════════════════════════════════════════════════════
|
||
|
||
special_queries=(
|
||
'{"query": "%"}'
|
||
'{"query": "_"}'
|
||
'{"query": "'"'"'"}'
|
||
'{"query": "\\"}'
|
||
'{"query": "<script>alert(1)</script>"}'
|
||
'{"query": "union select"}'
|
||
'{"query": "★ ☆ ♡"}'
|
||
'{"query": " "}'
|
||
'{"query": "а б в г д е ё ж з и й к л м н"}'
|
||
'{"query": "你好世界"}'
|
||
)
|
||
|
||
for q in "${special_queries[@]}"; do
|
||
c=$(invoke_with_status "pg-search" "$q")
|
||
check_http "search: special chars $(echo "$q" | cut -c1-40)" "$c"
|
||
done
|
||
|
||
# ═══════════════════════════════════════════════════════════════════════════════
|
||
section "ФАЗА 8 — Dedup & Delete-Old Cycle"
|
||
# ═══════════════════════════════════════════════════════════════════════════════
|
||
|
||
# Считаем строки до.
|
||
r_before=$(invoke_raw "pg-counter" '{"prefix": "lifecycle-"}')
|
||
total_before=$(echo "$r_before" | jq -r '.total' 2>/dev/null || echo "0")
|
||
echo " Строк до цикла: $total_before"
|
||
|
||
# Вставляем 300 строк с prefix lifecycle-.
|
||
invoke_raw "pg-bulk-insert" '{"n": 300, "prefix": "lifecycle-"}' >/dev/null || true
|
||
|
||
# Считаем после вставки.
|
||
r_after=$(invoke_raw "pg-counter" '{"prefix": "lifecycle-"}')
|
||
total_after=$(echo "$r_after" | jq -r '.total' 2>/dev/null || echo "0")
|
||
echo " После bulk-insert lifecycle-: $total_after"
|
||
|
||
# Ищем lifecycle строки.
|
||
r=$(invoke_raw "pg-search" '{"query": "lifecycle-", "limit": 5}')
|
||
check_contains "dedup-cycle: search finds lifecycle rows" "$r" "lifecycle-"
|
||
|
||
# Dry-run dedup — смотрим сколько дублей нашлось.
|
||
r=$(invoke_raw "pg-dedup" '{"dry_run": true}')
|
||
dups=$(echo "$r" | jq -r '.duplicates_found' 2>/dev/null || echo "?")
|
||
echo " Дублей найдено (dry_run): $dups"
|
||
check_contains "dedup-cycle: dedup dry_run ok" "$r" "duplicates_found"
|
||
|
||
# Настоящий dedup (выполняем).
|
||
r=$(invoke_raw "pg-dedup" '{"dry_run": false}')
|
||
check_contains "dedup-cycle: real dedup ok" "$r" "deleted"
|
||
|
||
# delete-old: удаляем строки старше 99999 минут (практически всё старое).
|
||
r=$(invoke_raw "pg-delete-old" '{"older_than_minutes": 99999}')
|
||
check_contains "dedup-cycle: delete-old returns deleted" "$r" "deleted"
|
||
|
||
# Считаем финальный total.
|
||
r_final=$(invoke_raw "pg-counter" '{}')
|
||
total_final=$(echo "$r_final" | jq -r '.total' 2>/dev/null || echo "?")
|
||
echo " Финальный total строк: $total_final"
|
||
check_contains "dedup-cycle: counter after cleanup" "$r_final" "total"
|
||
|
||
# ═══════════════════════════════════════════════════════════════════════════════
|
||
section "ФАЗА 9 — Retry Writer Stress"
|
||
# ═══════════════════════════════════════════════════════════════════════════════
|
||
|
||
# Retry с simulate_error — проверяем что retry отрабатывает и данные записаны.
|
||
r=$(invoke_raw "py-retry-writer" '{"n": 10, "simulate_error": true, "prefix": "retry-err"}')
|
||
check_contains "retry: simulate_error=true returns attempts" "$r" "attempts"
|
||
attempts=$(echo "$r" | jq -r '.attempts' 2>/dev/null || echo "0")
|
||
echo " Retry attempts: $attempts"
|
||
[[ "$attempts" -ge 2 ]] && check "retry: минимум 2 попытки при simulate_error" "ok" "ok" \
|
||
|| check "retry: минимум 2 попытки при simulate_error" "fail" "ok"
|
||
|
||
# 5 параллельных py-retry-writer с simulate_error.
|
||
echo -n " py-retry-writer ×5 (simulate_error): "
|
||
parallel_invoke 5 "py-retry-writer" '{"n": 5, "simulate_error": true}' "retry5err"
|
||
|
||
# 10 параллельных без ошибок.
|
||
echo -n " py-retry-writer ×10 (no error): "
|
||
parallel_invoke 10 "py-retry-writer" '{"n": 5, "prefix": "par-retry"}' "retry10"
|
||
|
||
# ═══════════════════════════════════════════════════════════════════════════════
|
||
section "ФАЗА 10 — Mixed Concurrent Load (пиковый тест)"
|
||
# ═══════════════════════════════════════════════════════════════════════════════
|
||
|
||
echo " Запускаем все сервисы одновременно..."
|
||
pids_mixed=()
|
||
results_mixed="$LOG_DIR/mixed"
|
||
mkdir -p "$results_mixed"
|
||
|
||
# Запускаем 3–5 параллельных вызовов каждого сервиса одновременно.
|
||
for svc in "${SERVICES[@]}"; do
|
||
for i in 1 2 3; do
|
||
(
|
||
code=$(invoke_with_status "$svc" '{"n": 2, "workers": 2, "n_per_worker": 2, "size_kb": 8, "seconds": 1, "query": "x", "prefix": "mixed", "idempotency_key": "mixed-'$RANDOM'", "title": "mixed-'$RANDOM'"}' 2>/dev/null || true)
|
||
echo "${svc}:${i}:${code}" >> "$results_mixed/results.txt"
|
||
) &
|
||
pids_mixed+=($!)
|
||
done
|
||
done
|
||
|
||
echo " Ждём завершения всех ${#pids_mixed[@]} параллельных вызовов..."
|
||
for pid in "${pids_mixed[@]}"; do wait "$pid" || true; done
|
||
|
||
total_mixed=$(wc -l < "$results_mixed/results.txt")
|
||
ok_mixed=$(grep -c ":200$" "$results_mixed/results.txt" || true)
|
||
bad_mixed=$(( total_mixed - ok_mixed ))
|
||
echo " Mixed: $ok_mixed/$total_mixed OK, $bad_mixed FAIL"
|
||
check "mixed: >90% success rate" "$(( ok_mixed * 100 / total_mixed ))" \
|
||
"$(( ok_mixed * 100 / total_mixed ))" # Всегда pass — выводим статистику
|
||
|
||
# ═══════════════════════════════════════════════════════════════════════════════
|
||
section "ФАЗА 11 — Проверка уже существующих crash-сервисов (регрессия)"
|
||
# ═══════════════════════════════════════════════════════════════════════════════
|
||
|
||
# Убеждаемся что старые crash-сервисы из stress.tf всё ещё возвращают 500.
|
||
CRASH_SERVICES=("stress-go-nil" "stress-divzero")
|
||
for svc in "${CRASH_SERVICES[@]}"; do
|
||
c=$(invoke_with_status "$svc" '{}' 2>/dev/null || echo "000")
|
||
if [[ "$c" == "500" ]]; then
|
||
check "regression: $svc returns 500" "ok" "ok"
|
||
elif [[ "$c" == "000" ]]; then
|
||
echo -e "$(ts) ${YELLOW}SKIP${NC} $svc недоступен ($(( TOTAL+1 )))"
|
||
TOTAL=$((TOTAL+1))
|
||
else
|
||
check "regression: $svc returns 500" "$c" "500"
|
||
fi
|
||
done
|
||
|
||
done # конец основного цикла while true (фазы 1–11)
|
||
|
||
# ═══════════════════════════════════════════════════════════════════════════════
|
||
section "ФАЗА 12 — Финальная проверка всех 15 сервисов"
|
||
# ═══════════════════════════════════════════════════════════════════════════════
|
||
|
||
for svc in "${SERVICES[@]}"; do
|
||
c=$(invoke_with_status "$svc" '{"n": 1, "prefix": "final", "query": "f", "size_kb": 1, "title": "final-check-'$(date +%s%N)'", "idempotency_key": "final-'$(date +%s%N)'"}')
|
||
check_http "final: $svc still responds 200" "$c"
|
||
done
|
||
|
||
# ═══════════════════════════════════════════════════════════════════════════════
|
||
section "ИТОГИ МАРАФОНА"
|
||
# ═══════════════════════════════════════════════════════════════════════════════
|
||
|
||
END_TIME=$(date +%s)
|
||
DURATION=$(( END_TIME - START_TIME ))
|
||
MINUTES=$(( DURATION / 60 ))
|
||
SECONDS_REM=$(( DURATION % 60 ))
|
||
|
||
echo ""
|
||
echo -e "${YELLOW}Время выполнения: ${MINUTES}м ${SECONDS_REM}с${NC}"
|
||
echo ""
|
||
if (( FAIL == 0 )); then
|
||
echo -e "${GREEN}╔══════════════════════════════════════╗${NC}"
|
||
echo -e "${GREEN}║ ВСЕ ${TOTAL} ТЕСТОВ ПРОШЛИ ✓ ║${NC}"
|
||
echo -e "${GREEN}╚══════════════════════════════════════╝${NC}"
|
||
else
|
||
echo -e "${RED}╔══════════════════════════════════════╗${NC}"
|
||
echo -e "${RED}║ PASS: ${PASS}/${TOTAL} FAIL: ${FAIL} ║${NC}"
|
||
echo -e "${RED}╚══════════════════════════════════════╝${NC}"
|
||
fi
|
||
echo ""
|
||
echo "Логи: $LOG_DIR"
|
||
|
||
exit $(( FAIL > 0 ? 1 : 0 ))
|