feat(service): timeout_sec без дефолта; 0=нет таймаута; operator v0.1.48

- api/v1alpha1/service_types.go: убрать +kubebuilder:default=30
- invoke.go: TimeoutSec=0 → &http.Client{} (без таймаута)
- services.go: валидация timeout_sec < 0 || > 900 → HTTP 400
- service_resource.go: TF schema Optional (без Computed); 0 → Int64Null()
- deployments/k8s/operator.yaml: v0.1.47 → v0.1.48
- doc/: progress.md + api/design.md (модель Service) + decisions/log.md
- examples/POSTGRES/: bug_hunter.sh, chaos_marathon.sh, chaos_marathon.tf
This commit is contained in:
Naeel
2026-03-21 16:58:43 +03:00
parent 7f7aa44e59
commit b86ff3a62e
37 changed files with 2493 additions and 50 deletions
+668
View File
@@ -0,0 +1,668 @@
#!/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 ))