From f6aaf15245a321a5534c4df7611160ee306b0297 Mon Sep 17 00:00:00 2001 From: Naeel Date: Sun, 22 Mar 2026 06:55:31 +0300 Subject: [PATCH] =?UTF-8?q?test:=20survival+pg=20=E2=80=94=20group=2011=20?= =?UTF-8?q?PG=20STORM=20+=20=D0=BF=D0=B0=D1=80=D0=B0=D0=BC=D0=B5=D1=82?= =?UTF-8?q?=D1=80=D0=B8=D0=B7=D0=B0=D1=86=D0=B8=D1=8F=20TOKEN/NS/PG?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - TOKEN/NS параметризованы через env SLESS_TOKEN / SLESS_NS - PG_HOST/PORT/USER/PASSWORD/DATABASE/TABLE добавлены в CONFIG - make_python_pg_zip: Python handler с psycopg2 (init/insert/count) - create_pg_deploy, pg_invoke_burst, pg_count_fn, pg_init_fn - Group 11 PG STORM: 11 тестов, 300+ параллельных INSERT/COUNT - Итоговый banner обновлён (G11 + PG STORM label) --- operator_survival_test.sh | 258 +++++++++++++++++++++++++++++++++++++- 1 file changed, 253 insertions(+), 5 deletions(-) diff --git a/operator_survival_test.sh b/operator_survival_test.sh index 2a081e7..671d21c 100644 --- a/operator_survival_test.sh +++ b/operator_survival_test.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -# 2026-03-21 — operator_survival_test.sh +# 2026-03-22 — operator_survival_test.sh # ТЕСТ НА ВЫЖИВАНИЕ — суровые многоуровневые нагрузочные сценарии оператора. # # Группа 5: FLOOD BUILD — 6 функций параллельно (3 Python + 3 Node.js), все до Ready @@ -8,21 +8,37 @@ # Группа 8: PHOENIX — 3 полных цикла DELETE+recreate одного имени # Группа 9: SELF-HEALING MARATHON — 5 раз вручную убить Deployment, ждать восстановления # Группа 10: INVOKE HURRICANE — 500 параллельных invokes в 5 волнах, считаем success rate +# Группа 11: PG STORM — 300+ параллельных INSERT/COUNT под нагрузкой; таблица test_ # -# Ожидаемое время прогона: 75-100 минут. +# Ожидаемое время прогона: 90-120 минут. # # ЗАПУСКАТЬ НА VM: # nohup bash ~/terra/sless/operator_survival_test.sh > /tmp/survival.log 2>&1 & # tail -f /tmp/survival.log # +# Параметризация через env (для multi-user wrapper): +# SLESS_TOKEN — JWT токен (по умолчанию из ~/terra/sless/test.token) +# SLESS_NS — namespace (по умолчанию sless-ffd1f598c169b0ae) +# PG_HOST / PG_PORT / PG_USER / PG_PASSWORD / PG_DATABASE — для группы 11 +# # Зависимости: curl, python3, zip, kubectl set -uo pipefail # ─── CONFIG ────────────────────────────────────────────────────────────────── -TOKEN=$(cat /home/naeel/terra/sless/test.token) -NS="sless-ffd1f598c169b0ae" +# Параметризация: дефолты переопределяются через env при multi-user запуске +TOKEN="${SLESS_TOKEN:-$(cat /home/naeel/terra/sless/test.token)}" +NS="${SLESS_NS:-sless-ffd1f598c169b0ae}" + +# PG кредентиалы для Group 11 — хардкод для теста, env переопределяет при multi-user +PG_HOST="${PG_HOST:-postgresqlk8s-master.bba55e0f-4c51-4312-8ad3-efc557de331d.svc.cluster.local}" +PG_PORT="${PG_PORT:-5432}" +PG_USER="${PG_USER:-user0}" +PG_PASSWORD="${PG_PASSWORD:-iuEHVxxIPfdFr2kWWmSfNJbgCmDzuEC1rQy38aCNj0rYF23BHguhtKFUAioR1Z4F}" +PG_DATABASE="${PG_DATABASE:-db0}" +# Имя таблицы: test_ — изоляция по namespace; hyphens→underscores для SQL +PG_TABLE="test_$(echo "$NS" | tr '-' '_')" API="https://sless.kube5s.ru/v1/namespaces/$NS" FN_BASE="https://sless.kube5s.ru/fn/$NS" DEPLOY_NS="sless-fn-$NS" @@ -249,6 +265,127 @@ invoke_burst() { echo "SUCCESS=$s FAIL=$f" } +# make_python_pg_zip NAME → /tmp/surv_py_pg_{NAME}.zip +# Python PostgreSQL handler: поддерживает action=init/insert/count. +# Имя таблицы берётся из env PG_TABLE, передаваемого при деплое. +make_python_pg_zip() { + local name="$1" + local tmpdir; tmpdir=$(mktemp -d) + cat > "$tmpdir/handler.py" <<'PYEOF' +import os, json, psycopg2 + +# Имя таблицы задаётся через env PG_TABLE при деплое — гарантирует изоляцию namespace +TABLE = os.environ.get("PG_TABLE", "test_default") + +def _connect(): + return psycopg2.connect( + host=os.environ["PGHOST"], + port=int(os.environ.get("PGPORT", "5432")), + dbname=os.environ["PGDATABASE"], + user=os.environ["PGUSER"], + password=os.environ["PGPASSWORD"], + sslmode=os.environ.get("PGSSLMODE", "require"), + ) + +def handle(event): + action = event.get("action", "insert") + val = str(event.get("val", "survival")) + conn = _connect() + try: + with conn: + with conn.cursor() as cur: + if action == "init": + cur.execute( + "CREATE TABLE IF NOT EXISTS {} " + "(id SERIAL PRIMARY KEY, val TEXT, ts TIMESTAMPTZ DEFAULT NOW())".format(TABLE) + ) + cur.execute("SELECT COUNT(*) FROM {}".format(TABLE)) + return {"ok": True, "action": "init", "count": cur.fetchone()[0]} + elif action == "insert": + cur.execute("INSERT INTO {} (val) VALUES (%s)".format(TABLE), (val,)) + return {"ok": True, "action": "insert"} + elif action == "count": + cur.execute("SELECT COUNT(*) FROM {}".format(TABLE)) + return {"ok": True, "action": "count", "count": cur.fetchone()[0]} + else: + return {"ok": False, "error": "unknown action: " + action} + finally: + conn.close() +PYEOF + # psycopg2-binary устанавливается через requirements.txt при сборке Docker-образа функции + printf 'psycopg2-binary==2.9.9\n' > "$tmpdir/requirements.txt" + (cd "$tmpdir" && zip -q "/tmp/surv_py_pg_${name}.zip" handler.py requirements.txt) + rm -rf "$tmpdir" +} + +# create_pg_deploy NAME TABLE — создать + загрузить PG функцию с нужными env vars +# PG переменные берутся из глобального CONFIG скрипта (или env override при multi-user) +create_pg_deploy() { + local name="$1" table="$2" + local envj + envj=$(printf \ + '{"PGHOST":"%s","PGPORT":"%s","PGDATABASE":"%s","PGUSER":"%s","PGPASSWORD":"%s","PGSSLMODE":"require","PG_TABLE":"%s"}' \ + "$PG_HOST" "$PG_PORT" "$PG_DATABASE" "$PG_USER" "$PG_PASSWORD" "$table") + local http + http=$(api_code POST "$API/services" \ + "{\"name\":\"$name\",\"display_name\":\"pg storm test\",\ +\"runtime\":\"python3.11\",\"entrypoint\":\"handler.handle\",\ +\"memory_mb\":256,\"env_vars\":$envj}") + [[ "$http" == "201" ]] || { fail "create PG $name → HTTP $http"; return 1; } + make_python_pg_zip "$name" + local ucode + ucode=$(upload_zip "$name" "/tmp/surv_py_pg_${name}.zip") + [[ "$ucode" == "200" ]] || { fail "upload PG $name → HTTP $ucode"; return 1; } + return 0 +} + +# pg_invoke_burst NAME COUNT PARALLEL ACTION — параллельные invokes с заданным PG action +# Выводит "SUCCESS=N FAIL=M" +pg_invoke_burst() { + local name="$1" total="$2" parallel="$3" action="${4:-insert}" + local tmp; tmp=$(mktemp -d) + local idx=0 + while [[ $idx -lt $total ]]; do + local pids=() batch=0 + while [[ $batch -lt $parallel && $idx -lt $total ]]; do + ( + code=$(curl -s -o /dev/null -w "%{http_code}" -m 30 \ + -X POST -H "Content-Type: application/json" \ + -d "{\"action\":\"${action}\",\"val\":\"burst-${idx}\"}" \ + "$FN_BASE/$name") + echo "$code" > "$tmp/${idx}" + ) & + pids+=($!); idx=$((idx+1)); batch=$((batch+1)) + done + wait "${pids[@]}" + done + local s=0 f=0 + for file in "$tmp"/*; do + c=$(cat "$file") + [[ "$c" == "200" ]] && s=$((s+1)) || f=$((f+1)) + done + rm -rf "$tmp" + echo "SUCCESS=$s FAIL=$f" +} + +# pg_count_fn NAME → число строк таблицы (из ответа PG функции) +pg_count_fn() { + local name="$1" + curl -s -m 30 -X POST -H "Content-Type: application/json" \ + -d '{"action":"count"}' "$FN_BASE/$name" \ + | python3 -c 'import sys,json; print(json.load(sys.stdin).get("count",0))' 2>/dev/null \ + || echo "0" +} + +# pg_init_fn NAME → "True"/"False" по полю ok ответа +pg_init_fn() { + local name="$1" + curl -s -m 60 -X POST -H "Content-Type: application/json" \ + -d '{"action":"init"}' "$FN_BASE/$name" \ + | python3 -c 'import sys,json; print(json.load(sys.stdin).get("ok",False))' 2>/dev/null \ + || echo "False" +} + # deploy_ns_has NAME → 0 если Deployment существует deploy_ns_has() { kubectl get deployment -n "$DEPLOY_NS" "$1" --no-headers 2>/dev/null | grep -q "$1" @@ -896,6 +1033,116 @@ info "10.5 Удаляем hurricane-функцию..." check_code "DELETE $HU" "$(api_code DELETE "$API/services/$HU")" "204" CLEANUP_NAMES=( "${CLEANUP_NAMES[@]/$HU}" ) +# ═══════════════════════════════════════════════════════════════════════════════ +section 11 "PG STORM — 300+ параллельных INSERT/COUNT; таблица $PG_TABLE" +# ═══════════════════════════════════════════════════════════════════════════════ +# Цель: проверить что Python-функция с psycopg2 выдерживает конкурентные +# INSERT/SELECT в свою изолированную таблицу test_. +# Данные не должны теряться под нагрузкой; счётчик строк должен расти корректно. + +PG_FN="pgstorm-${SFX}" +CLEANUP_NAMES+=("$PG_FN") + +info "11.1 Создаём + загружаем PG функцию $PG_FN (psycopg2, таблица $PG_TABLE)..." +if create_pg_deploy "$PG_FN" "$PG_TABLE"; then + pass "create+upload $PG_FN → OK" + + info "11.2 Ждём Ready $PG_FN (300s — pip install psycopg2 в image build)..." + if wait_phase "$PG_FN" "Ready" 300; then + pass "$PG_FN достиг Ready ✓" + else + fail "$PG_FN НЕ достиг Ready за 300s" + fi + kubectl rollout status deployment/"$PG_FN" -n "$DEPLOY_NS" --timeout=60s \ + > /dev/null 2>&1 || true + sleep 3 + + info "11.3 init → CREATE TABLE IF NOT EXISTS $PG_TABLE..." + pg_init_result=$(pg_init_fn "$PG_FN") + if [[ "$pg_init_result" == "True" ]]; then + pass "init → таблица $PG_TABLE создана / уже существует ✓" + else + fail "init → неожиданный ответ: $pg_init_result" + fi + + info "11.4 50 последовательных INSERT — базовая проверка без ошибок..." + seq_ok=0; seq_fail=0 + for i in $(seq 1 50); do + resp=$(curl -s -m 30 -X POST -H "Content-Type: application/json" \ + -d "{\"action\":\"insert\",\"val\":\"seq-${i}\"}" "$FN_BASE/$PG_FN" 2>/dev/null) + ok=$(echo "$resp" | python3 -c \ + 'import sys,json; print(json.load(sys.stdin).get("ok",False))' 2>/dev/null) + [[ "$ok" == "True" ]] && seq_ok=$((seq_ok+1)) || seq_fail=$((seq_fail+1)) + done + if [[ $seq_ok -eq 50 ]]; then + pass "50/50 последовательных INSERT → все OK ✓" + else + fail "последовательные INSERT: $seq_ok/50 OK, $seq_fail упало" + fi + + info "11.5 COUNT после 50 INSERT → ожидаем ≥ 50..." + cnt1=$(pg_count_fn "$PG_FN") + if [[ "${cnt1:-0}" -ge 50 ]]; then + pass "COUNT=$cnt1 ≥ 50 ✓" + else + fail "COUNT=$cnt1 < 50 (данные потеряны?)" + fi + + info "11.6 100 параллельных INSERT (pmax=20) — первая волна под давлением..." + res6=$(pg_invoke_burst "$PG_FN" 100 20 "insert") + s6="${res6#*SUCCESS=}"; s6="${s6%% *}"; s6="${s6:-0}" + f6="${res6#*FAIL=}"; f6="${f6:-0}" + pct6=$(( s6 * 100 / 100 )) + info " Волна 1: $s6/100 OK ($pct6%), $f6 FAIL" + if [[ $pct6 -ge 90 ]]; then + pass "INSERT волна 1: $s6/100 → ${pct6}% (≥90%) ✓" + else + fail "INSERT волна 1: $s6/100 → ${pct6}% (<90%)" + fi + + info "11.7 COUNT после волны 1 → ожидаем ≥ 140..." + cnt2=$(pg_count_fn "$PG_FN") + if [[ "${cnt2:-0}" -ge 140 ]]; then + pass "COUNT=$cnt2 ≥ 140 ✓" + else + fail "COUNT=$cnt2 < 140 (данные теряются под нагрузкой?)" + fi + + info "11.8 150 параллельных INSERT (pmax=30) — вторая волна, жёче..." + res8=$(pg_invoke_burst "$PG_FN" 150 30 "insert") + s8="${res8#*SUCCESS=}"; s8="${s8%% *}"; s8="${s8:-0}" + f8="${res8#*FAIL=}"; f8="${f8:-0}" + pct8=$(( s8 * 100 / 150 )) + info " Волна 2: $s8/150 OK ($pct8%), $f8 FAIL" + if [[ $pct8 -ge 85 ]]; then + pass "INSERT волна 2: $s8/150 → ${pct8}% (≥85%) ✓" + else + fail "INSERT волна 2: $s8/150 → ${pct8}% (<85%)" + fi + + info "11.9 50 параллельных COUNT (pmax=25) — read-нагрузка..." + res9=$(pg_invoke_burst "$PG_FN" 50 25 "count") + sc="${res9#*SUCCESS=}"; sc="${sc%% *}"; sc="${sc:-0}" + pc=$(( sc * 100 / 50 )) + if [[ $pc -ge 90 ]]; then + pass "COUNT под нагрузкой: $sc/50 → ${pc}% (≥90%) ✓" + else + fail "COUNT под нагрузкой: $sc/50 → ${pc}% (<90%)" + fi + + info "11.10 Финальный COUNT → ожидаем ≥ 270 (50 + 90%×100 + 85%×150)..." + cnt3=$(pg_count_fn "$PG_FN") + if [[ "${cnt3:-0}" -ge 270 ]]; then + pass "финальный COUNT=$cnt3 ≥ 270 ✓" + else + fail "финальный COUNT=$cnt3 < 270" + fi + + info "11.11 Удаляем $PG_FN (таблица $PG_TABLE остаётся в PostgreSQL)..." + check_code "DELETE $PG_FN" "$(api_code DELETE "$API/services/$PG_FN")" "204" + CLEANUP_NAMES=( "${CLEANUP_NAMES[@]/$PG_FN}" ) +fi + # ═══════════════════════════════════════════════════════════════════════════════ echo "" echo -e "${BOLD}╔══════════════════════════════════════════════════════╗${RESET}" @@ -910,7 +1157,7 @@ echo " FAIL : $FAIL" echo "" # Per-group итоги -for grp in G5 G6 G7 G8 G9 G10; do +for grp in G5 G6 G7 G8 G9 G10 G11; do g_pass=${GRP_PASS[$grp]:-0} g_fail=${GRP_FAIL[$grp]:-0} g_total=$((g_pass + g_fail)) @@ -919,6 +1166,7 @@ for grp in G5 G6 G7 G8 G9 G10; do G5) label="FLOOD BUILD";; G6) label="RAPID DISCARD STORM";; G7) label="CHURN UNDER FIRE";; G8) label="PHOENIX";; G9) label="SELF-HEALING MARATHON";; G10) label="INVOKE HURRICANE";; + G11) label="PG STORM";; esac if [[ $g_fail -eq 0 ]]; then echo -e " ${GREEN}✓${RESET} Группа $grp ($label): ${g_pass}/${g_total}"