1185 lines
50 KiB
Bash
1185 lines
50 KiB
Bash
#!/usr/bin/env bash
|
||
# 2026-03-22 — operator_survival_test.sh
|
||
# ТЕСТ НА ВЫЖИВАНИЕ — суровые многоуровневые нагрузочные сценарии оператора.
|
||
#
|
||
# Группа 5: FLOOD BUILD — 6 функций параллельно (3 Python + 3 Node.js), все до Ready
|
||
# Группа 6: RAPID DISCARD STORM — 30 create→DELETE без build, проверка на мусор
|
||
# Группа 7: CHURN UNDER FIRE — 10 PUT с меняющимися env под 200 параллельными invokes
|
||
# Группа 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>
|
||
#
|
||
# Ожидаемое время прогона: 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 ──────────────────────────────────────────────────────────────────
|
||
|
||
# Параметризация: дефолты переопределяются через 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"
|
||
# Уникальный префикс — не пересекается с prod-сервисами и lifecycle-тестом
|
||
TS=$(date +%s)
|
||
SFX="sv-${TS}" # sv = survival
|
||
|
||
# ─── СТАТИСТИКА (глобальная) ──────────────────────────────────────────────────
|
||
|
||
PASS=0; FAIL=0
|
||
declare -A GRP_PASS GRP_FAIL # per-group counters
|
||
|
||
# ─── ЦВЕТА ───────────────────────────────────────────────────────────────────
|
||
|
||
GREEN='\033[0;32m'; RED='\033[0;31m'; YELLOW='\033[1;33m'
|
||
CYAN='\033[0;36m'; BOLD='\033[1m'; RESET='\033[0m'
|
||
|
||
# ─── HELPERS ─────────────────────────────────────────────────────────────────
|
||
|
||
_cur_grp="" # текущая группа для per-group счётчиков
|
||
|
||
pass() {
|
||
echo -e " ${GREEN}[PASS]${RESET} $1"
|
||
PASS=$((PASS + 1))
|
||
[[ -n "$_cur_grp" ]] && GRP_PASS[$_cur_grp]=$(( ${GRP_PASS[$_cur_grp]:-0} + 1 ))
|
||
}
|
||
fail() {
|
||
echo -e " ${RED}[FAIL]${RESET} $1"
|
||
FAIL=$((FAIL + 1))
|
||
[[ -n "$_cur_grp" ]] && GRP_FAIL[$_cur_grp]=$(( ${GRP_FAIL[$_cur_grp]:-0} + 1 ))
|
||
}
|
||
info() { echo -e " ${CYAN}[INFO]${RESET} $1"; }
|
||
warn() { echo -e " ${YELLOW}[WARN]${RESET} $1"; }
|
||
section() {
|
||
_cur_grp="G$1"
|
||
echo -e "\n${BOLD}━━━ ГРУППА $1: $2 ━━━${RESET}"
|
||
GRP_PASS[$_cur_grp]=0; GRP_FAIL[$_cur_grp]=0
|
||
}
|
||
|
||
# api_code METHOD URL [body] → HTTP status code
|
||
api_code() {
|
||
local method="$1" url="$2" body="${3:-}"
|
||
local args=(-s -o /dev/null -w "%{http_code}" -m 120
|
||
-H "Authorization: Bearer $TOKEN")
|
||
[[ "$method" != "GET" ]] && args+=(-X "$method")
|
||
[[ -n "$body" ]] && args+=(-H "Content-Type: application/json" -d "$body")
|
||
curl "${args[@]}" "$url"
|
||
}
|
||
|
||
# api_json METHOD URL [body] → response body
|
||
api_json() {
|
||
local method="$1" url="$2" body="${3:-}"
|
||
local args=(-s -m 120 -H "Authorization: Bearer $TOKEN")
|
||
[[ "$method" != "GET" ]] && args+=(-X "$method")
|
||
[[ -n "$body" ]] && args+=(-H "Content-Type: application/json" -d "$body")
|
||
curl "${args[@]}" "$url"
|
||
}
|
||
|
||
check_code() {
|
||
local label="$1" got="$2" want="$3"
|
||
if [[ "$got" == "$want" ]]; then pass "$label → HTTP $got"
|
||
else fail "$label → HTTP $got (ожидали $want)"; fi
|
||
}
|
||
|
||
# svc_phase NAME → phase string
|
||
svc_phase() {
|
||
api_json GET "$API/services/$1" \
|
||
| python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('phase',''))" 2>/dev/null
|
||
}
|
||
|
||
# wait_phase NAME WANT TIMEOUT_SEC → 0=ok 1=timeout 2=Failed
|
||
wait_phase() {
|
||
local name="$1" want="$2" timeout_sec="${3:-300}"
|
||
local deadline=$((SECONDS + timeout_sec))
|
||
while [[ $SECONDS -lt $deadline ]]; do
|
||
local phase
|
||
phase=$(svc_phase "$name")
|
||
[[ "$phase" == "$want" ]] && return 0
|
||
[[ "$phase" == "Failed" && "$want" != "Failed" ]] && return 2
|
||
sleep 5
|
||
done
|
||
return 1
|
||
}
|
||
|
||
# wait_all_ready TIMEOUT NAME1 NAME2 ... → число сколько НЕ достигло Ready
|
||
wait_all_ready() {
|
||
local timeout_sec="$1"; shift
|
||
local names=("$@")
|
||
local deadline=$((SECONDS + timeout_sec))
|
||
local not_ready=( "${names[@]}" )
|
||
while [[ $SECONDS -lt $deadline && ${#not_ready[@]} -gt 0 ]]; do
|
||
local still=()
|
||
for n in "${not_ready[@]}"; do
|
||
local p
|
||
p=$(svc_phase "$n")
|
||
[[ "$p" == "Ready" ]] || still+=("$n")
|
||
done
|
||
not_ready=( "${still[@]}" )
|
||
[[ ${#not_ready[@]} -eq 0 ]] && break
|
||
sleep 5
|
||
done
|
||
echo "${#not_ready[@]}" # количество не дошедших до Ready
|
||
}
|
||
|
||
# make_python_zip NAME → /tmp/surv_py_{NAME}.zip
|
||
# Python echo-handler: возвращает {"ok":true,"env":TEST_VAR,"grp":GRP_VAR}
|
||
make_python_zip() {
|
||
local name="$1"
|
||
local tmpdir; tmpdir=$(mktemp -d)
|
||
cat > "$tmpdir/handler.py" <<'PYEOF'
|
||
import os, json
|
||
|
||
def handle(event):
|
||
return {
|
||
"ok": True,
|
||
"env": os.environ.get("TEST_VAR", ""),
|
||
"grp": os.environ.get("GRP_VAR", ""),
|
||
"msg": event.get("msg", ""),
|
||
"iter": event.get("iter", 0),
|
||
}
|
||
PYEOF
|
||
(cd "$tmpdir" && zip -q "/tmp/surv_py_${name}.zip" handler.py)
|
||
rm -rf "$tmpdir"
|
||
}
|
||
|
||
# make_node_zip NAME → /tmp/surv_node_{NAME}.zip
|
||
make_node_zip() {
|
||
local name="$1"
|
||
local tmpdir; tmpdir=$(mktemp -d)
|
||
cat > "$tmpdir/handler.js" <<'JSEOF'
|
||
'use strict';
|
||
exports.handle = function(event) {
|
||
return {
|
||
ok: true,
|
||
env: process.env.TEST_VAR || '',
|
||
grp: process.env.GRP_VAR || '',
|
||
msg: (event && event.msg) || '',
|
||
iter: (event && event.iter) || 0,
|
||
};
|
||
};
|
||
JSEOF
|
||
(cd "$tmpdir" && zip -q "/tmp/surv_node_${name}.zip" handler.js)
|
||
rm -rf "$tmpdir"
|
||
}
|
||
|
||
# upload_zip NAME ZIPFILE → HTTP code
|
||
upload_zip() {
|
||
local name="$1" zipfile="$2"
|
||
curl -s -o /dev/null -w "%{http_code}" -m 120 \
|
||
-H "Authorization: Bearer $TOKEN" \
|
||
-F "code=@$zipfile" \
|
||
"$API/services/$name/upload"
|
||
}
|
||
|
||
# create_py_deploy NAME MEMORY ENV_VARS_JSON — create + upload python handler
|
||
# return: 0=ok, 1=fail
|
||
create_py_deploy() {
|
||
local name="$1" mem="${2:-128}" envj="${3:-{}}"
|
||
local http
|
||
http=$(api_code POST "$API/services" \
|
||
"{\"name\":\"$name\",\"display_name\":\"survival test\",\
|
||
\"runtime\":\"python3.11\",\"entrypoint\":\"handler.handle\",\
|
||
\"memory_mb\":$mem,\"env_vars\":$envj}")
|
||
[[ "$http" == "201" ]] || { fail "create $name → HTTP $http"; return 1; }
|
||
make_python_zip "$name"
|
||
local ucode
|
||
ucode=$(upload_zip "$name" "/tmp/surv_py_${name}.zip")
|
||
[[ "$ucode" == "200" ]] || { fail "upload $name → HTTP $ucode"; return 1; }
|
||
return 0
|
||
}
|
||
|
||
# create_node_deploy NAME MEMORY ENV_VARS_JSON — create + upload node handler
|
||
create_node_deploy() {
|
||
local name="$1" mem="${2:-128}" envj="${3:-{}}"
|
||
local http
|
||
http=$(api_code POST "$API/services" \
|
||
"{\"name\":\"$name\",\"display_name\":\"survival test\",\
|
||
\"runtime\":\"nodejs20\",\"entrypoint\":\"handler.handle\",\
|
||
\"memory_mb\":$mem,\"env_vars\":$envj}")
|
||
[[ "$http" == "201" ]] || { fail "create $name → HTTP $http"; return 1; }
|
||
make_node_zip "$name"
|
||
local ucode
|
||
ucode=$(upload_zip "$name" "/tmp/surv_node_${name}.zip")
|
||
[[ "$ucode" == "200" ]] || { fail "upload $name → HTTP $ucode"; return 1; }
|
||
return 0
|
||
}
|
||
|
||
# invoke_one NAME → HTTP code
|
||
invoke_one() {
|
||
local name="$1"
|
||
curl -s -o /dev/null -w "%{http_code}" -m 30 \
|
||
-X POST -H "Content-Type: application/json" \
|
||
-d '{"msg":"survival"}' \
|
||
"$FN_BASE/$name"
|
||
}
|
||
|
||
# invoke_burst NAME COUNT PARALLEL — запустить COUNT запросов пачками PARALLEL
|
||
# выводит "SUCCESS=N FAIL=M"
|
||
invoke_burst() {
|
||
local name="$1" total="$2" parallel="$3"
|
||
local results_dir
|
||
results_dir=$(mktemp -d)
|
||
local idx=0
|
||
while [[ $idx -lt $total ]]; do
|
||
local batch_pids=()
|
||
local batch=0
|
||
while [[ $batch -lt $parallel && $idx -lt $total ]]; do
|
||
(
|
||
local code
|
||
code=$(invoke_one "$name")
|
||
echo "$code" > "$results_dir/${idx}"
|
||
) &
|
||
batch_pids+=($!)
|
||
idx=$((idx + 1)); batch=$((batch + 1))
|
||
done
|
||
wait "${batch_pids[@]}"
|
||
done
|
||
local s=0 f=0
|
||
for file in "$results_dir"/*; do
|
||
local c; c=$(cat "$file")
|
||
if [[ "$c" == "200" ]]; then s=$((s + 1)); else f=$((f + 1)); fi
|
||
done
|
||
rm -rf "$results_dir"
|
||
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"
|
||
}
|
||
|
||
# cleanup NAME — удалить сервис (teardown)
|
||
cleanup() {
|
||
local name="$1"
|
||
[[ -z "$name" ]] && return 0 # пустое имя — пропускаем
|
||
api_code DELETE "$API/services/$name" > /dev/null 2>&1 || true
|
||
sleep 2
|
||
}
|
||
|
||
# Список для teardown — заполняется по ходу тестов
|
||
CLEANUP_NAMES=()
|
||
teardown() {
|
||
if [[ ${#CLEANUP_NAMES[@]} -gt 0 ]]; then
|
||
echo -e "\n${CYAN}[TEARDOWN]${RESET} удаляю тестовые сервисы..."
|
||
for name in "${CLEANUP_NAMES[@]}"; do
|
||
[[ -z "$name" ]] && continue # пропускаем пустые элементы
|
||
cleanup "$name"
|
||
echo -e " ${CYAN}[TEARDOWN]${RESET} удалён: $name"
|
||
done
|
||
fi
|
||
}
|
||
trap teardown EXIT
|
||
|
||
# ─── СТАРТОВЫЙ БАННЕР ────────────────────────────────────────────────────────
|
||
|
||
echo ""
|
||
echo -e "${BOLD}╔══════════════════════════════════════════════════════╗${RESET}"
|
||
echo -e "${BOLD}║ OPERATOR SURVIVAL TEST — sless v0.1.49 ║${RESET}"
|
||
echo -e "${BOLD}╚══════════════════════════════════════════════════════╝${RESET}"
|
||
echo -e " Дата : $(TZ=Asia/Dubai date '+%Y-%m-%d %H:%M:%S') (GMT+4)"
|
||
echo -e " API : $API"
|
||
echo -e " SFX : $SFX"
|
||
echo ""
|
||
|
||
# ═══════════════════════════════════════════════════════════════════════════════
|
||
section 5 "FLOOD BUILD — 6 функций параллельно (3 × Python + 3 × Node.js)"
|
||
# ═══════════════════════════════════════════════════════════════════════════════
|
||
# Цель: убедиться что много одновременных builds не мешают друг другу,
|
||
# все достигают Ready, все отвечают на invoke.
|
||
|
||
FB1="fb1-${SFX}"; FB2="fb2-${SFX}"; FB3="fb3-${SFX}"
|
||
FB4="fb4-${SFX}"; FB5="fb5-${SFX}"; FB6="fb6-${SFX}"
|
||
FB_ALL=("$FB1" "$FB2" "$FB3" "$FB4" "$FB5" "$FB6")
|
||
CLEANUP_NAMES+=( "${FB_ALL[@]}" )
|
||
|
||
info "5.1 CREATE + UPLOAD 6 функций параллельно (3 Python / 3 Node.js)..."
|
||
|
||
(set +e; create_py_deploy "$FB1" 128 '{"GRP_VAR":"py1"}') &
|
||
(set +e; create_py_deploy "$FB2" 192 '{"GRP_VAR":"py2"}') &
|
||
(set +e; create_py_deploy "$FB3" 256 '{"GRP_VAR":"py3"}') &
|
||
(set +e; create_node_deploy "$FB4" 128 '{"GRP_VAR":"nd1"}') &
|
||
(set +e; create_node_deploy "$FB5" 192 '{"GRP_VAR":"nd2"}') &
|
||
(set +e; create_node_deploy "$FB6" 256 '{"GRP_VAR":"nd3"}') &
|
||
wait
|
||
|
||
info "5.2 Проверяем что все 6 CRD созданы (GET → 200)..."
|
||
# Небольшая пауза — контроллер устанавливает phase асинхронно после create
|
||
sleep 5
|
||
all_created=true
|
||
for name in "${FB_ALL[@]}"; do
|
||
http_check=$(api_code GET "$API/services/$name")
|
||
if [[ "$http_check" != "200" ]]; then
|
||
fail "CRD $name не существует → HTTP $http_check (create/upload провалился)"
|
||
all_created=false
|
||
fi
|
||
done
|
||
$all_created && pass "все 6 CRD созданы"
|
||
|
||
info "5.3 Ждём ALL READY (450s для 6 параллельных builds)..."
|
||
not_ready_count=$(wait_all_ready 450 "${FB_ALL[@]}")
|
||
if [[ "$not_ready_count" -eq 0 ]]; then
|
||
pass "все 6 функций достигли Ready ✓"
|
||
else
|
||
fail "$not_ready_count из 6 НЕ достигли Ready за 450s"
|
||
fi
|
||
|
||
# Ждём rollout всех Deployment
|
||
info "5.4 Ждём rollout всех 6 Deployment (120s)..."
|
||
all_rollout=true
|
||
for name in "${FB_ALL[@]}"; do
|
||
if ! kubectl rollout status deployment/"$name" -n "$DEPLOY_NS" --timeout=120s \
|
||
> /dev/null 2>&1; then
|
||
warn "$name rollout не завершился"
|
||
all_rollout=false
|
||
fi
|
||
done
|
||
sleep 5
|
||
$all_rollout && pass "все 6 Deployment rollout завершён"
|
||
|
||
info "5.5 Python-функции: invoke × 3 к каждой, проверяем ok=True..."
|
||
py_ok=true
|
||
for name in "$FB1" "$FB2" "$FB3"; do
|
||
for _i in 1 2 3; do
|
||
rv=$(curl -s -m 20 -X POST -H "Content-Type: application/json" \
|
||
-d '{"msg":"flood"}' "$FN_BASE/$name")
|
||
ok_val=$(echo "$rv" | python3 -c \
|
||
"import sys,json; d=json.load(sys.stdin); print(d.get('ok',''))" 2>/dev/null)
|
||
if [[ "$ok_val" != "True" ]]; then
|
||
fail "$name invoke[$_i] → ok='$ok_val' (resp: ${rv:0:100})"
|
||
py_ok=false; break
|
||
fi
|
||
done
|
||
done
|
||
$py_ok && pass "все Python-функции invoke → ok=True"
|
||
|
||
info "5.6 Node.js-функции: invoke × 3 к каждой, проверяем ok=true..."
|
||
nd_ok=true
|
||
for name in "$FB4" "$FB5" "$FB6"; do
|
||
for _i in 1 2 3; do
|
||
rv=$(curl -s -m 20 -X POST -H "Content-Type: application/json" \
|
||
-d '{"msg":"flood"}' "$FN_BASE/$name")
|
||
ok_val=$(echo "$rv" | python3 -c \
|
||
"import sys,json; d=json.load(sys.stdin); print(str(d.get('ok','')).lower())" 2>/dev/null)
|
||
if [[ "$ok_val" != "true" ]]; then
|
||
fail "$name invoke[$_i] → ok='$ok_val' (resp: ${rv:0:100})"
|
||
nd_ok=false; break
|
||
fi
|
||
done
|
||
done
|
||
$nd_ok && pass "все Node.js-функции invoke → ok=true"
|
||
|
||
info "5.7 GRP_VAR env vars различаются у каждой функции..."
|
||
env_ok=true
|
||
declare -A EXPECTED_GRP
|
||
EXPECTED_GRP["$FB1"]="py1"; EXPECTED_GRP["$FB2"]="py2"; EXPECTED_GRP["$FB3"]="py3"
|
||
EXPECTED_GRP["$FB4"]="nd1"; EXPECTED_GRP["$FB5"]="nd2"; EXPECTED_GRP["$FB6"]="nd3"
|
||
for name in "${FB_ALL[@]}"; do
|
||
rv=$(curl -s -m 20 -X POST -H "Content-Type: application/json" \
|
||
-d '{}' "$FN_BASE/$name")
|
||
got_grp=$(echo "$rv" | python3 -c \
|
||
"import sys,json; d=json.load(sys.stdin); print(d.get('grp',''))" 2>/dev/null)
|
||
want="${EXPECTED_GRP[$name]}"
|
||
if [[ "$got_grp" != "$want" ]]; then
|
||
fail "$name grp='$got_grp' (ожидали '$want')"
|
||
env_ok=false
|
||
fi
|
||
done
|
||
$env_ok && pass "GRP_VAR корректен у всех 6 функций"
|
||
|
||
info "5.8 memory_mb применён в k8s Deployments..."
|
||
mem_ok=true
|
||
declare -A EXPECTED_MEM
|
||
EXPECTED_MEM["$FB1"]="128Mi"; EXPECTED_MEM["$FB2"]="192Mi"; EXPECTED_MEM["$FB3"]="256Mi"
|
||
EXPECTED_MEM["$FB4"]="128Mi"; EXPECTED_MEM["$FB5"]="192Mi"; EXPECTED_MEM["$FB6"]="256Mi"
|
||
for name in "${FB_ALL[@]}"; do
|
||
got_mem=$(kubectl get deployment "$name" -n "$DEPLOY_NS" \
|
||
-o jsonpath='{.spec.template.spec.containers[0].resources.limits.memory}' 2>/dev/null)
|
||
want="${EXPECTED_MEM[$name]}"
|
||
if [[ "$got_mem" != "$want" ]]; then
|
||
fail "$name k8s memory='$got_mem' (ожидали '$want')"
|
||
mem_ok=false
|
||
fi
|
||
done
|
||
$mem_ok && pass "k8s memory limits корректны у всех 6 функций"
|
||
|
||
info "5.9 Удаляем все 6 flood-функций параллельно..."
|
||
del_ok=true
|
||
for name in "${FB_ALL[@]}"; do
|
||
(api_code DELETE "$API/services/$name" > /dev/null 2>&1) &
|
||
done
|
||
wait
|
||
sleep 5
|
||
for name in "${FB_ALL[@]}"; do
|
||
code=$(api_code GET "$API/services/$name")
|
||
[[ "$code" == "404" ]] || { fail "$name: GET после DELETE → $code (ожидали 404)"; del_ok=false; }
|
||
done
|
||
$del_ok && pass "все 6 функций удалены, GET → 404"
|
||
# Убираем из cleanup (уже удалены) — фильтруем пустые и fb-имена
|
||
new_cleanup=()
|
||
for e in "${CLEANUP_NAMES[@]}"; do
|
||
skip=false
|
||
for fn in "${FB_ALL[@]}"; do [[ "$e" == "$fn" ]] && skip=true; done
|
||
[[ -z "$e" ]] && skip=true
|
||
$skip || new_cleanup+=("$e")
|
||
done
|
||
CLEANUP_NAMES=( "${new_cleanup[@]}" )
|
||
|
||
# Ждём очистки k8s
|
||
sleep 15
|
||
|
||
# ═══════════════════════════════════════════════════════════════════════════════
|
||
section 6 "RAPID DISCARD STORM — 30 create→DELETE без build"
|
||
# ═══════════════════════════════════════════════════════════════════════════════
|
||
# Цель: убедиться что 30 быстрых create+delete не оставляют orphan CRD.
|
||
# Кейс: пользователь создал функцию, сразу передумал и удалил без upload.
|
||
|
||
info "6.1 Создаём + сразу удаляем 30 функций (10 параллельно, 3 волны)..."
|
||
rd_creates_ok=0
|
||
rd_deletes_ok=0
|
||
rd_total=30
|
||
|
||
results_dir_6=$(mktemp -d)
|
||
overflow=0
|
||
for wave in 1 2 3; do
|
||
wave_pids=()
|
||
for slot in $(seq 1 10); do
|
||
idx=$(( (wave - 1) * 10 + slot ))
|
||
(
|
||
set +e
|
||
rname="rd${idx}-${SFX}"
|
||
# CREATE
|
||
http_c=$(api_code POST "$API/services" \
|
||
"{\"name\":\"$rname\",\"runtime\":\"python3.11\",\
|
||
\"entrypoint\":\"h.h\",\"memory_mb\":128,\"env_vars\":{}}")
|
||
# Немедленно DELETE — не ждём Building
|
||
http_d=$(api_code DELETE "$API/services/$rname")
|
||
echo "${http_c}:${http_d}" > "$results_dir_6/${idx}"
|
||
) &
|
||
wave_pids+=($!)
|
||
done
|
||
wait "${wave_pids[@]}"
|
||
done
|
||
|
||
# Подсчёт результатов
|
||
for file in "$results_dir_6"/*; do
|
||
pair=$(cat "$file")
|
||
c_code="${pair%%:*}"
|
||
d_code="${pair##*:}"
|
||
[[ "$c_code" == "201" ]] && rd_creates_ok=$((rd_creates_ok + 1))
|
||
[[ "$d_code" == "204" || "$d_code" == "404" ]] && rd_deletes_ok=$((rd_deletes_ok + 1))
|
||
done
|
||
rm -rf "$results_dir_6"
|
||
|
||
if [[ $rd_creates_ok -ge 27 ]]; then # допустим ≤3 ошибки (гонка)
|
||
pass "$rd_creates_ok/$rd_total create вернули 201"
|
||
else
|
||
fail "$rd_creates_ok/$rd_total create вернули 201 (ожидали ≥27)"
|
||
fi
|
||
if [[ $rd_deletes_ok -ge 27 ]]; then
|
||
pass "$rd_deletes_ok/$rd_total delete вернули 204/404"
|
||
else
|
||
fail "$rd_deletes_ok/$rd_total delete ок (ожидали ≥27)"
|
||
fi
|
||
|
||
info "6.2 Ждём 20s чтобы finalizer-ы отработали..."
|
||
sleep 20
|
||
|
||
info "6.3 Проверяем что нет orphan CRD в namespace $NS..."
|
||
orphan_count=$(api_json GET "$API/services" \
|
||
| python3 -c "
|
||
import sys, json
|
||
data = json.load(sys.stdin)
|
||
items = data if isinstance(data, list) else data.get('items', data.get('services', []))
|
||
surv = [i.get('name','') for i in items if '${SFX}' in i.get('name','')]
|
||
print(len(surv))
|
||
print('\n'.join(surv))
|
||
" 2>/dev/null | head -1)
|
||
if [[ "$orphan_count" -eq 0 ]]; then
|
||
pass "нет orphan CRD — все $rd_total функций удалены чисто"
|
||
else
|
||
fail "$orphan_count orphan CRD обнаружено после rapid discard"
|
||
fi
|
||
|
||
info "6.4 Нет orphan Deployment/Service в deploy namespace $DEPLOY_NS..."
|
||
orphan_k8s=$(kubectl get deployment -n "$DEPLOY_NS" --no-headers 2>/dev/null \
|
||
| grep "${SFX}" | wc -l)
|
||
if [[ "$orphan_k8s" -eq 0 ]]; then
|
||
pass "нет orphan Deployment в $DEPLOY_NS"
|
||
else
|
||
fail "$orphan_k8s orphan Deployment в $DEPLOY_NS после rapid discard"
|
||
fi
|
||
|
||
# ═══════════════════════════════════════════════════════════════════════════════
|
||
section 7 "CHURN UNDER FIRE — 10 PUT env-updates под 200 параллельными invokes"
|
||
# ═══════════════════════════════════════════════════════════════════════════════
|
||
# Цель: обновления конфигурации не ломают работу функции под нагрузкой.
|
||
# Метрика: success rate invoke >= 90%, финальный env корректный.
|
||
|
||
CU="churn-${SFX}"
|
||
CLEANUP_NAMES+=("$CU")
|
||
|
||
info "7.1 Создаём и деплоим churn-функцию..."
|
||
if create_py_deploy "$CU" 192 '{"TEST_VAR":"ver-0","GRP_VAR":"churn"}'; then
|
||
pass "create+upload $CU OK"
|
||
else
|
||
fail "create+upload $CU провалился, пропускаем группу 7"
|
||
# Очистка + переход к следующей группе
|
||
cleanup "$CU"
|
||
fi
|
||
|
||
info "7.2 Ждём Ready (300s)..."
|
||
cu_ready=0
|
||
wait_phase "$CU" "Ready" 300 || cu_ready=$?
|
||
if [[ $cu_ready -eq 0 ]]; then
|
||
pass "$CU Ready"
|
||
kubectl rollout status deployment/"$CU" -n "$DEPLOY_NS" --timeout=60s \
|
||
> /dev/null 2>&1 || true
|
||
sleep 3
|
||
else
|
||
fail "$CU не вышел в Ready (фаза: $(svc_phase "$CU")), пропускаем тест"
|
||
cleanup "$CU"; CLEANUP_NAMES=( "${CLEANUP_NAMES[@]/$CU}" )
|
||
# jump over group 7 остальные тесты пропустим
|
||
cu_ready=99
|
||
fi
|
||
|
||
if [[ $cu_ready -eq 0 ]]; then
|
||
info "7.3 Запускаем фоновый поток: 200 invoke (10 параллельно × 20 волн)..."
|
||
# Invoke в фоне — пишем коды в файлы
|
||
churn_results=$(mktemp -d)
|
||
(
|
||
for wave in $(seq 1 20); do
|
||
wave_pids=()
|
||
for _b in $(seq 1 10); do
|
||
idx=$(( (wave - 1) * 10 + _b ))
|
||
(
|
||
code=$(invoke_one "$CU")
|
||
echo "$code" > "$churn_results/${idx}"
|
||
) &
|
||
wave_pids+=($!)
|
||
done
|
||
wait "${wave_pids[@]}"
|
||
sleep 0.5 # небольшая пауза между волнами чтобы не throttle
|
||
done
|
||
) &
|
||
fire_pid=$!
|
||
|
||
info "7.4 Пока идут invoke — делаем 10 PUT с нарастающим TEST_VAR..."
|
||
for ver in $(seq 1 10); do
|
||
sleep 4 # ~каждые 4s новый PUT
|
||
http_put=$(api_code PUT "$API/services/$CU" \
|
||
"{\"runtime\":\"python3.11\",\"entrypoint\":\"handler.handle\",\
|
||
\"memory_mb\":192,\"env_vars\":{\"TEST_VAR\":\"ver-${ver}\",\"GRP_VAR\":\"churn\"}}")
|
||
if [[ "$http_put" == "200" ]]; then
|
||
info " PUT ver-${ver} → 200 ✓"
|
||
else
|
||
fail "PUT ver-${ver} → HTTP $http_put"
|
||
fi
|
||
done
|
||
|
||
info "7.5 Ждём завершения фонового invoke-потока..."
|
||
wait $fire_pid
|
||
|
||
# Подсчёт invoke результатов
|
||
cu_success=0; cu_fail_cnt=0
|
||
for file in "$churn_results"/*; do
|
||
code=$(cat "$file")
|
||
if [[ "$code" == "200" ]]; then cu_success=$((cu_success + 1))
|
||
else cu_fail_cnt=$((cu_fail_cnt + 1)); fi
|
||
done
|
||
rm -rf "$churn_results"
|
||
total_invokes=$((cu_success + cu_fail_cnt))
|
||
success_pct=0
|
||
[[ $total_invokes -gt 0 ]] && success_pct=$(( cu_success * 100 / total_invokes ))
|
||
info " Invoke сводка: $cu_success/$total_invokes успешных ($success_pct%)"
|
||
|
||
if [[ $success_pct -ge 90 ]]; then
|
||
pass "invoke success rate $success_pct% ≥ 90% под 10 rolling updates"
|
||
else
|
||
fail "invoke success rate $success_pct% < 90% (много гонок при update?)"
|
||
fi
|
||
|
||
info "7.6 Ждём rollout финального обновления (ver-10)..."
|
||
sleep 10
|
||
kubectl rollout status deployment/"$CU" -n "$DEPLOY_NS" --timeout=90s \
|
||
> /dev/null 2>&1 || true
|
||
sleep 5
|
||
|
||
info "7.7 Финальный invoke → TEST_VAR должен быть ver-10..."
|
||
final_resp=$(curl -s -m 20 -X POST -H "Content-Type: application/json" \
|
||
-d '{}' "$FN_BASE/$CU")
|
||
final_env=$(echo "$final_resp" | python3 -c \
|
||
"import sys,json; d=json.load(sys.stdin); print(d.get('env',''))" 2>/dev/null)
|
||
if [[ "$final_env" == "ver-10" ]]; then
|
||
pass "финальный env=ver-10 ✓"
|
||
else
|
||
fail "финальный env='$final_env' (ожидали 'ver-10')"
|
||
fi
|
||
|
||
info "7.8 k8s Deployment env = ver-10..."
|
||
k8s_env_cu=$(kubectl get deployment "$CU" -n "$DEPLOY_NS" -o json 2>/dev/null \
|
||
| python3 -c "import sys,json
|
||
d=json.load(sys.stdin)
|
||
envs=d['spec']['template']['spec']['containers'][0].get('env',[])
|
||
val=next((e['value'] for e in envs if e['name']=='TEST_VAR'), '')
|
||
print(val)" 2>/dev/null)
|
||
if [[ "$k8s_env_cu" == "ver-10" ]]; then
|
||
pass "k8s Deployment TEST_VAR=ver-10 ✓"
|
||
else
|
||
fail "k8s Deployment TEST_VAR='$k8s_env_cu' (ожидали 'ver-10')"
|
||
fi
|
||
fi
|
||
|
||
info "7.9 Удаляем churn-функцию..."
|
||
check_code "DELETE $CU" "$(api_code DELETE "$API/services/$CU")" "204"
|
||
CLEANUP_NAMES=( "${CLEANUP_NAMES[@]/$CU}" )
|
||
|
||
# ═══════════════════════════════════════════════════════════════════════════════
|
||
section 8 "PHOENIX — 3 цикла DELETE + recreate одного имени"
|
||
# ═══════════════════════════════════════════════════════════════════════════════
|
||
# Цель: после DELETE сервиса его имя можно переиспользовать немедленно.
|
||
# Финалайзер должен корректно удалить k8s ресурсы прежде чем CRD исчезнет.
|
||
# Каждый цикл: create → wait Ready → DELETE → wait 404 → снова create.
|
||
|
||
PX="phoenix-${SFX}"
|
||
CLEANUP_NAMES+=("$PX")
|
||
|
||
phoenix_all_ok=true
|
||
for cycle in 1 2 3; do
|
||
info "8.${cycle} PHOENIX цикл $cycle/3..."
|
||
|
||
# CREATE
|
||
cx_code=$(api_code POST "$API/services" \
|
||
"{\"name\":\"$PX\",\"display_name\":\"phoenix c$cycle\",\
|
||
\"runtime\":\"python3.11\",\"entrypoint\":\"handler.handle\",\
|
||
\"memory_mb\":128,\"env_vars\":{\"TEST_VAR\":\"cycle-${cycle}\"}}")
|
||
if [[ "$cx_code" != "201" ]]; then
|
||
fail " phoenix цикл $cycle: CREATE → HTTP $cx_code (ожидали 201)"
|
||
phoenix_all_ok=false; continue
|
||
fi
|
||
|
||
# UPLOAD
|
||
make_python_zip "${PX}_cycle${cycle}"
|
||
cp "/tmp/surv_py_${PX}_cycle${cycle}.zip" "/tmp/surv_py_${PX}.zip" 2>/dev/null || true
|
||
make_python_zip "${PX}"
|
||
u_code=$(upload_zip "$PX" "/tmp/surv_py_${PX}.zip")
|
||
if [[ "$u_code" != "200" ]]; then
|
||
fail " phoenix цикл $cycle: UPLOAD → HTTP $u_code"
|
||
phoenix_all_ok=false; cleanup "$PX"; continue
|
||
fi
|
||
|
||
# WAIT READY
|
||
px_w=0
|
||
wait_phase "$PX" "Ready" 300 || px_w=$?
|
||
if [[ $px_w -ne 0 ]]; then
|
||
fail " phoenix цикл $cycle: не дошёл до Ready (w=$px_w, phase=$(svc_phase "$PX"))"
|
||
phoenix_all_ok=false; cleanup "$PX"; continue
|
||
fi
|
||
kubectl rollout status deployment/"$PX" -n "$DEPLOY_NS" --timeout=60s \
|
||
> /dev/null 2>&1 || true
|
||
sleep 3
|
||
|
||
# INVOKE — убедиться что работает и env правильный
|
||
px_resp=$(curl -s -m 20 -X POST -H "Content-Type: application/json" \
|
||
-d '{}' "$FN_BASE/$PX")
|
||
px_env=$(echo "$px_resp" | python3 -c \
|
||
"import sys,json; d=json.load(sys.stdin); print(d.get('env',''))" 2>/dev/null)
|
||
if [[ "$px_env" == "cycle-${cycle}" ]]; then
|
||
pass " phoenix цикл $cycle: Ready + invoke env=cycle-${cycle} ✓"
|
||
else
|
||
fail " phoenix цикл $cycle: invoke env='$px_env' (ожидали 'cycle-${cycle}')"
|
||
phoenix_all_ok=false
|
||
fi
|
||
|
||
# DELETE
|
||
del_code=$(api_code DELETE "$API/services/$PX")
|
||
if [[ "$del_code" != "204" ]]; then
|
||
fail " phoenix цикл $cycle: DELETE → HTTP $del_code"
|
||
phoenix_all_ok=false; continue
|
||
fi
|
||
|
||
# WAIT 404 — ждём когда finalizer уберёт CRD
|
||
info " Ждём когда CRD исчезнет (finalizer, 60s)..."
|
||
gone=false
|
||
for _w in $(seq 1 12); do
|
||
sleep 5
|
||
code404=$(api_code GET "$API/services/$PX")
|
||
if [[ "$code404" == "404" ]]; then gone=true; break; fi
|
||
done
|
||
if ! $gone; then
|
||
fail " phoenix цикл $cycle: CRD $PX не исчез за 60s после DELETE"
|
||
phoenix_all_ok=false
|
||
fi
|
||
|
||
# Проверяем k8s Deployment тоже удалён
|
||
k8s_gone=false
|
||
for _w in $(seq 1 6); do
|
||
sleep 5
|
||
deploy_ns_has "$PX" || { k8s_gone=true; break; }
|
||
done
|
||
if ! $k8s_gone; then
|
||
fail " phoenix цикл $cycle: k8s Deployment не удалён за 30s"
|
||
phoenix_all_ok=false
|
||
fi
|
||
|
||
info " Цикл $cycle завершён — имя $PX освободилось"
|
||
done
|
||
|
||
if $phoenix_all_ok; then
|
||
pass "PHOENIX: все 3 цикла DELETE+recreate прошли успешно"
|
||
fi
|
||
CLEANUP_NAMES=( "${CLEANUP_NAMES[@]/$PX}" )
|
||
|
||
# ═══════════════════════════════════════════════════════════════════════════════
|
||
section 9 "SELF-HEALING MARATHON — 5 раз вручную убить Deployment"
|
||
# ═══════════════════════════════════════════════════════════════════════════════
|
||
# Цель: оператор с RequeueAfter:60s восстанавливает Deployment каждый раз.
|
||
# Без сбоев подряд: удаляем → ждём 90s → убедиться что пересоздан → invoke OK.
|
||
|
||
SH="sheal-${SFX}"
|
||
CLEANUP_NAMES+=("$SH")
|
||
|
||
info "9.1 Создаём и деплоим self-healing функцию..."
|
||
if create_py_deploy "$SH" 128 '{"TEST_VAR":"sheal"}'; then
|
||
sh_ready=0
|
||
wait_phase "$SH" "Ready" 300 || sh_ready=$?
|
||
if [[ $sh_ready -eq 0 ]]; then
|
||
pass "$SH Ready перед self-healing marathon"
|
||
kubectl rollout status deployment/"$SH" -n "$DEPLOY_NS" --timeout=60s \
|
||
> /dev/null 2>&1 || true
|
||
sleep 3
|
||
else
|
||
fail "$SH не вышел в Ready, пропускаем группу 9"
|
||
sh_ready=99
|
||
fi
|
||
else
|
||
fail "create/upload $SH провалился, пропускаем группу 9"
|
||
sh_ready=99
|
||
fi
|
||
|
||
if [[ "${sh_ready:-0}" -eq 0 ]]; then
|
||
sheal_ok=0
|
||
for kill_round in 1 2 3 4 5; do
|
||
info "9.kill$kill_round — убиваем Deployment (раунд $kill_round/5)..."
|
||
kubectl delete deployment "$SH" -n "$DEPLOY_NS" > /dev/null 2>&1
|
||
|
||
info " Ждём пересоздания (90s = минимум один RequeueAfter: 60s цикл + delta)..."
|
||
recreated=false
|
||
for _w in $(seq 1 18); do
|
||
sleep 5
|
||
if deploy_ns_has "$SH"; then recreated=true; break; fi
|
||
done
|
||
|
||
if $recreated; then
|
||
# Ждём pod готовности
|
||
kubectl rollout status deployment/"$SH" -n "$DEPLOY_NS" --timeout=90s \
|
||
> /dev/null 2>&1 || true
|
||
sleep 3
|
||
# Invoke должен работать
|
||
sh_resp=$(curl -s -m 30 -X POST -H "Content-Type: application/json" \
|
||
-d '{"iter":'"$kill_round"'}' "$FN_BASE/$SH")
|
||
sh_ok=$(echo "$sh_resp" | python3 -c \
|
||
"import sys,json; d=json.load(sys.stdin); print(d.get('ok',''))" 2>/dev/null)
|
||
if [[ "$sh_ok" == "True" ]]; then
|
||
pass " раунд $kill_round: Deployment пересоздан + invoke OK"
|
||
sheal_ok=$((sheal_ok + 1))
|
||
else
|
||
fail " раунд $kill_round: Deployment пересоздан, но invoke → ${sh_resp:0:80}"
|
||
fi
|
||
else
|
||
fail " раунд $kill_round: Deployment НЕ пересоздан за 90s после удаления"
|
||
fi
|
||
done
|
||
info "Self-healing итог: $sheal_ok/5 раундов прошли"
|
||
fi
|
||
|
||
info "9.final Удаляем $SH..."
|
||
check_code "DELETE $SH" "$(api_code DELETE "$API/services/$SH")" "204"
|
||
CLEANUP_NAMES=( "${CLEANUP_NAMES[@]/$SH}" )
|
||
|
||
# ═══════════════════════════════════════════════════════════════════════════════
|
||
section 10 "INVOKE HURRICANE — 500 параллельных invokes в 5 волнах"
|
||
# ═══════════════════════════════════════════════════════════════════════════════
|
||
# Цель: функция держит нагрузку 100 одновременных запросов × 5 волн = 500 total.
|
||
# Метрика: success rate >= 90%, нет зависаний.
|
||
|
||
HU="hurr-${SFX}"
|
||
CLEANUP_NAMES+=("$HU")
|
||
|
||
info "10.1 Создаём hurricane-функцию (Node.js, 256Mi)..."
|
||
if create_node_deploy "$HU" 256 '{"TEST_VAR":"hurricane","GRP_VAR":"load"}'; then
|
||
hu_ready=0
|
||
wait_phase "$HU" "Ready" 300 || hu_ready=$?
|
||
if [[ $hu_ready -eq 0 ]]; then
|
||
pass "$HU Ready"
|
||
kubectl rollout status deployment/"$HU" -n "$DEPLOY_NS" --timeout=90s \
|
||
> /dev/null 2>&1 || true
|
||
sleep 5
|
||
else
|
||
fail "$HU не вышел в Ready, пропускаем группу 10"
|
||
hu_ready=99
|
||
fi
|
||
else
|
||
fail "create/upload $HU провалился, пропускаем группу 10"
|
||
hu_ready=99
|
||
fi
|
||
|
||
if [[ "${hu_ready:-0}" -eq 0 ]]; then
|
||
info "10.2 Прогрев: 10 последовательных invoke перед нагрузкой..."
|
||
warmup_ok=0
|
||
for _w in $(seq 1 10); do
|
||
code=$(invoke_one "$HU")
|
||
[[ "$code" == "200" ]] && warmup_ok=$((warmup_ok + 1))
|
||
done
|
||
if [[ $warmup_ok -ge 8 ]]; then
|
||
pass "прогрев: $warmup_ok/10 OK"
|
||
else
|
||
fail "прогрев: $warmup_ok/10 OK (функция не стабильна)"
|
||
fi
|
||
|
||
info "10.3 ВОЛНА 1-5: 100 параллельных запросов в каждой волне (500 total)..."
|
||
total_ok=0; total_fail=0
|
||
for wave in 1 2 3 4 5; do
|
||
info " ВОЛНА $wave/5 (100 параллельных)..."
|
||
# Запускаем 100 параллельно
|
||
wave_dir=$(mktemp -d)
|
||
wave_pids=()
|
||
for req in $(seq 1 100); do
|
||
(
|
||
code=$(invoke_one "$HU")
|
||
echo "$code" > "$wave_dir/${req}"
|
||
) &
|
||
wave_pids+=($!)
|
||
done
|
||
wait "${wave_pids[@]}"
|
||
|
||
wave_ok=0; wave_fail=0
|
||
for file in "$wave_dir"/*; do
|
||
c=$(cat "$file")
|
||
if [[ "$c" == "200" ]]; then wave_ok=$((wave_ok + 1))
|
||
else wave_fail=$((wave_fail + 1)); fi
|
||
done
|
||
rm -rf "$wave_dir"
|
||
total_ok=$((total_ok + wave_ok))
|
||
total_fail=$((total_fail + wave_fail))
|
||
info " волна $wave: $wave_ok/100 OK, $wave_fail FAIL"
|
||
sleep 2 # небольшой перерыв между волнами
|
||
done
|
||
|
||
total=$((total_ok + total_fail))
|
||
pct=0
|
||
[[ $total -gt 0 ]] && pct=$(( total_ok * 100 / total ))
|
||
info " Итого: $total_ok/$total OK ($pct%)"
|
||
|
||
if [[ $pct -ge 90 ]]; then
|
||
pass "HURRICANE: $total_ok/$total invoke → ${pct}% success rate (≥90%) ✓"
|
||
else
|
||
fail "HURRICANE: $total_ok/$total invoke → ${pct}% success rate (<90%)"
|
||
fi
|
||
|
||
info "10.4 Функция стабильна после шторма — финальный invoke..."
|
||
sleep 5
|
||
final_code=$(invoke_one "$HU")
|
||
if [[ "$final_code" == "200" ]]; then
|
||
pass "финальный invoke после нагрузки → 200 ✓"
|
||
else
|
||
fail "финальный invoke после нагрузки → $final_code"
|
||
fi
|
||
fi
|
||
|
||
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}"
|
||
echo -e "${BOLD}║ ИТОГИ SURVIVAL ║${RESET}"
|
||
echo -e "${BOLD}╚══════════════════════════════════════════════════════╝${RESET}"
|
||
echo ""
|
||
echo " Время завершения : $(TZ=Asia/Dubai date '+%Y-%m-%d %H:%M:%S') (GMT+4)"
|
||
echo ""
|
||
echo " Всего тестов : $((PASS + FAIL))"
|
||
echo " PASS : $PASS"
|
||
echo " FAIL : $FAIL"
|
||
echo ""
|
||
|
||
# Per-group итоги
|
||
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))
|
||
label=""
|
||
case "$grp" in
|
||
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}"
|
||
else
|
||
echo -e " ${RED}✗${RESET} Группа $grp ($label): ${g_pass}/${g_total} (FAIL: $g_fail)"
|
||
fi
|
||
done
|
||
|
||
echo ""
|
||
if [[ $FAIL -eq 0 ]]; then
|
||
echo -e " ${GREEN}${BOLD}✓ ВЫЖИЛ — ВСЕ ТЕСТЫ ПРОШЛИ${RESET}"
|
||
else
|
||
echo -e " ${RED}${BOLD}✗ ПРОВАЛ — $FAIL тестов упало${RESET}"
|
||
fi
|
||
echo ""
|