#!/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": ""}' '{"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 ))