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
|
#!/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
|
# Группа 5: FLOOD BUILD — 6 функций параллельно (3 Python + 3 Node.js), все до Ready
|
||||||
@@ -8,21 +8,37 @@
|
|||||||
# Группа 8: PHOENIX — 3 полных цикла DELETE+recreate одного имени
|
# Группа 8: PHOENIX — 3 полных цикла DELETE+recreate одного имени
|
||||||
# Группа 9: SELF-HEALING MARATHON — 5 раз вручную убить Deployment, ждать восстановления
|
# Группа 9: SELF-HEALING MARATHON — 5 раз вручную убить Deployment, ждать восстановления
|
||||||
# Группа 10: INVOKE HURRICANE — 500 параллельных invokes в 5 волнах, считаем success rate
|
# Группа 10: INVOKE HURRICANE — 500 параллельных invokes в 5 волнах, считаем success rate
|
||||||
|
# Группа 11: PG STORM — 300+ параллельных INSERT/COUNT под нагрузкой; таблица test_<NS>
|
||||||
#
|
#
|
||||||
# Ожидаемое время прогона: 75-100 минут.
|
# Ожидаемое время прогона: 90-120 минут.
|
||||||
#
|
#
|
||||||
# ЗАПУСКАТЬ НА VM:
|
# ЗАПУСКАТЬ НА VM:
|
||||||
# nohup bash ~/terra/sless/operator_survival_test.sh > /tmp/survival.log 2>&1 &
|
# nohup bash ~/terra/sless/operator_survival_test.sh > /tmp/survival.log 2>&1 &
|
||||||
# tail -f /tmp/survival.log
|
# 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
|
# Зависимости: curl, python3, zip, kubectl
|
||||||
|
|
||||||
set -uo pipefail
|
set -uo pipefail
|
||||||
|
|
||||||
# ─── CONFIG ──────────────────────────────────────────────────────────────────
|
# ─── CONFIG ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
TOKEN=$(cat /home/naeel/terra/sless/test.token)
|
# Параметризация: дефолты переопределяются через env при multi-user запуске
|
||||||
NS="sless-ffd1f598c169b0ae"
|
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"
|
API="https://sless.kube5s.ru/v1/namespaces/$NS"
|
||||||
FN_BASE="https://sless.kube5s.ru/fn/$NS"
|
FN_BASE="https://sless.kube5s.ru/fn/$NS"
|
||||||
DEPLOY_NS="sless-fn-$NS"
|
DEPLOY_NS="sless-fn-$NS"
|
||||||
@@ -249,6 +265,127 @@ invoke_burst() {
|
|||||||
echo "SUCCESS=$s FAIL=$f"
|
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 NAME → 0 если Deployment существует
|
||||||
deploy_ns_has() {
|
deploy_ns_has() {
|
||||||
kubectl get deployment -n "$DEPLOY_NS" "$1" --no-headers 2>/dev/null | grep -q "$1"
|
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"
|
check_code "DELETE $HU" "$(api_code DELETE "$API/services/$HU")" "204"
|
||||||
CLEANUP_NAMES=( "${CLEANUP_NAMES[@]/$HU}" )
|
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 ""
|
||||||
echo -e "${BOLD}╔══════════════════════════════════════════════════════╗${RESET}"
|
echo -e "${BOLD}╔══════════════════════════════════════════════════════╗${RESET}"
|
||||||
@@ -910,7 +1157,7 @@ echo " FAIL : $FAIL"
|
|||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
# Per-group итоги
|
# 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_pass=${GRP_PASS[$grp]:-0}
|
||||||
g_fail=${GRP_FAIL[$grp]:-0}
|
g_fail=${GRP_FAIL[$grp]:-0}
|
||||||
g_total=$((g_pass + g_fail))
|
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";;
|
G5) label="FLOOD BUILD";; G6) label="RAPID DISCARD STORM";;
|
||||||
G7) label="CHURN UNDER FIRE";; G8) label="PHOENIX";;
|
G7) label="CHURN UNDER FIRE";; G8) label="PHOENIX";;
|
||||||
G9) label="SELF-HEALING MARATHON";; G10) label="INVOKE HURRICANE";;
|
G9) label="SELF-HEALING MARATHON";; G10) label="INVOKE HURRICANE";;
|
||||||
|
G11) label="PG STORM";;
|
||||||
esac
|
esac
|
||||||
if [[ $g_fail -eq 0 ]]; then
|
if [[ $g_fail -eq 0 ]]; then
|
||||||
echo -e " ${GREEN}✓${RESET} Группа $grp ($label): ${g_pass}/${g_total}"
|
echo -e " ${GREEN}✓${RESET} Группа $grp ($label): ${g_pass}/${g_total}"
|
||||||
|
|||||||
Reference in New Issue
Block a user