test: survival+pg — group 11 PG STORM + параметризация TOKEN/NS/PG
- 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)
This commit is contained in:
+253
-5
@@ -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_<NS>
|
||||
#
|
||||
# Ожидаемое время прогона: 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_<NS> — изоляция по 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_<NS>.
|
||||
# Данные не должны теряться под нагрузкой; счётчик строк должен расти корректно.
|
||||
|
||||
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}"
|
||||
|
||||
Reference in New Issue
Block a user