From a25725fdb76f153072e7fa18f042b1c346c2c17c Mon Sep 17 00:00:00 2001 From: Naeel Date: Sat, 21 Mar 2026 19:16:30 +0300 Subject: [PATCH] =?UTF-8?q?test:=20operator=5Fsurvival=5Ftest.sh=20?= =?UTF-8?q?=E2=80=94=20survival=20suite=20(=D0=B3=D1=80=D1=83=D0=BF=D0=BF?= =?UTF-8?q?=D1=8B=205-10)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 5 FLOOD BUILD: 6 функций (3×Python + 3×Node.js) параллельно → все Ready 6 RAPID DISCARD STORM: 30 create→DELETE без build, orphan-check 7 CHURN UNDER FIRE: 10 PUT env-updates под 200 параллельными invokes, rate≥90% 8 PHOENIX: 3 цикла DELETE+recreate одного имени 9 SELF-HEALING MARATHON: 5×kubectl delete deployment → auto-recreate (RequeueAfter) 10 INVOKE HURRICANE: 500 параллельных invokes в 5 волнах (100 × 5), rate≥90% Ожидаемое время прогона: 75-100 минут --- operator_survival_test.sh | 928 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 928 insertions(+) create mode 100644 operator_survival_test.sh diff --git a/operator_survival_test.sh b/operator_survival_test.sh new file mode 100644 index 0000000..e17f39c --- /dev/null +++ b/operator_survival_test.sh @@ -0,0 +1,928 @@ +#!/usr/bin/env bash +# 2026-03-21 — 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 +# +# Ожидаемое время прогона: 75-100 минут. +# +# ЗАПУСКАТЬ НА VM: +# nohup bash ~/terra/sless/operator_survival_test.sh > /tmp/survival.log 2>&1 & +# tail -f /tmp/survival.log +# +# Зависимости: curl, python3, zip, kubectl + +set -uo pipefail + +# ─── CONFIG ────────────────────────────────────────────────────────────────── + +TOKEN=$(cat /home/naeel/terra/sless/test.token) +NS="sless-ffd1f598c169b0ae" +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" +} + +# 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" + 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 + 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 " Дата : $(date '+%Y-%m-%d %H:%M:%S')" +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 созданы..." +all_created=true +for name in "${FB_ALL[@]}"; do + ph=$(svc_phase "$name") + if [[ -z "$ph" ]]; then + fail "CRD $name не существует (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 (уже удалены) +CLEANUP_NAMES=( "${CLEANUP_NAMES[@]}" ) +for name in "${FB_ALL[@]}"; do + CLEANUP_NAMES=( "${CLEANUP_NAMES[@]/$name}" ) +done + +# Ждём очистки 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}" ) + +# ═══════════════════════════════════════════════════════════════════════════════ +echo "" +echo -e "${BOLD}╔══════════════════════════════════════════════════════╗${RESET}" +echo -e "${BOLD}║ ИТОГИ SURVIVAL ║${RESET}" +echo -e "${BOLD}╚══════════════════════════════════════════════════════╝${RESET}" +echo "" +echo " Время завершения : $(date '+%Y-%m-%d %H:%M:%S')" +echo "" +echo " Всего тестов : $((PASS + FAIL))" +echo " PASS : $PASS" +echo " FAIL : $FAIL" +echo "" + +# Per-group итоги +for grp in G5 G6 G7 G8 G9 G10; 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";; + 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 ""