diff --git a/tests/stress_test.sh b/tests/stress_test.sh new file mode 100644 index 0000000..94f2d1d --- /dev/null +++ b/tests/stress_test.sh @@ -0,0 +1,656 @@ +#!/bin/bash +# stress_test.sh — стресс-тест shared-sqs: конкурентность, Redis resilience, падение пода +# Created: 2026-04-11 +# Покрывает: +# 1. Параллельная отправка сообщений (N воркеров, M сообщений) +# 2. Параллельное чтение и удаление (гонка за сообщения) +# 3. Multi-tenant изоляция под нагрузкой (нет утечек между тенантами) +# 4. Потеря связи с Redis — поведение при разрыве +# 5. Kill пода — рестарт и восстановление данных из Redis +# 6. Burst — резкий всплеск запросов +# Запуск: BASE_URL=https://qu.kube5s.ru ADMIN_TOKEN=... bash tests/stress_test.sh + +set -uo pipefail + +# ═══════════════════════════════════════════ +# КОНФИГУРАЦИЯ +# ═══════════════════════════════════════════ +BASE_URL="${BASE_URL:-https://qu.kube5s.ru}" +ADMIN_TOKEN="${ADMIN_TOKEN:-sqs-admin-7a7d8bd0c060a75c198d48680f34077a}" +TS=$(date +%s) +NAMESPACE="shared-sqs" +DEPLOY_NAME="shared-sqs" + +# Параметры нагрузки +CONCURRENT_WORKERS=10 # параллельных воркеров +MESSAGES_PER_WORKER=20 # сообщений на воркер +BURST_SIZE=50 # размер burst-пакета +TENANT_COUNT=3 # тенантов для multi-tenant теста + +PASS=0 +FAIL=0 +TOTAL=0 +TMPDIR_STRESS=$(mktemp -d /tmp/sqs_stress_XXXXX) + +# ═══════════════════════════════════════════ +# ХЕЛПЕРЫ +# ═══════════════════════════════════════════ + +check() { + local name="$1" output="$2" pattern="$3" + TOTAL=$((TOTAL + 1)) + if echo "$output" | grep -qE "$pattern"; then + PASS=$((PASS + 1)) + echo " ✅ $name" + else + FAIL=$((FAIL + 1)) + echo " ❌ $name" + echo " ожидалось: $pattern" + echo " получено: $(echo "$output" | head -5)" + fi +} + +check_eq() { + local name="$1" got="$2" want="$3" + TOTAL=$((TOTAL + 1)) + if [[ "$got" == "$want" ]]; then + PASS=$((PASS + 1)) + echo " ✅ $name ($got)" + else + FAIL=$((FAIL + 1)) + echo " ❌ $name: ожидалось=$want, получено=$got" + fi +} + +check_ge() { + local name="$1" got="$2" want="$3" + TOTAL=$((TOTAL + 1)) + if [[ "$got" -ge "$want" ]]; then + PASS=$((PASS + 1)) + echo " ✅ $name ($got >= $want)" + else + FAIL=$((FAIL + 1)) + echo " ❌ $name: ожидалось >=$want, получено=$got" + fi +} + +# sqs — AWS CLI враппер +sqs() { + local ak="$1" sk="$2" + shift 2 + AWS_ACCESS_KEY_ID="$ak" \ + AWS_SECRET_ACCESS_KEY="$sk" \ + AWS_DEFAULT_REGION="us-east-1" \ + aws --endpoint-url "$BASE_URL" --output json sqs "$@" 2>&1 +} + +# admin_api — Admin REST API +admin_api() { + local method="$1" path="$2" body="${3:-}" + if [[ -n "$body" ]]; then + curl -s --max-time 20 -X "$method" \ + -H "Authorization: Bearer $ADMIN_TOKEN" \ + -H "Content-Type: application/json" \ + -d "$body" \ + "${BASE_URL}${path}" 2>&1 + else + curl -s --max-time 20 -X "$method" \ + -H "Authorization: Bearer $ADMIN_TOKEN" \ + "${BASE_URL}${path}" 2>&1 + fi +} + +# wait_for_pod_ready — ждёт пока под поднимется (max 120s) +wait_for_pod_ready() { + local max_wait=120 + local waited=0 + echo " ⏳ Ожидание пода (max ${max_wait}s)..." + while [[ $waited -lt $max_wait ]]; do + local ready + ready=$(kubectl get pods -n "$NAMESPACE" -l app.kubernetes.io/name=shared-sqs \ + -o jsonpath='{.items[0].status.conditions[?(@.type=="Ready")].status}' 2>/dev/null) + if [[ "$ready" == "True" ]]; then + echo " ✅ Под готов (${waited}s)" + return 0 + fi + sleep 2 + waited=$((waited + 2)) + done + echo " ⚠️ Под не готов после ${max_wait}s" + return 1 +} + +# wait_for_healthy — ждёт пока сервис отвечает на /health +wait_for_healthy() { + local max_wait=120 + local waited=0 + echo " ⏳ Ожидание /health (max ${max_wait}s)..." + while [[ $waited -lt $max_wait ]]; do + local status + status=$(curl -s -o /dev/null -w "%{http_code}" --max-time 5 "${BASE_URL}/health" 2>/dev/null) + if [[ "$status" == "200" ]]; then + echo " ✅ Сервис отвечает (${waited}s)" + return 0 + fi + sleep 2 + waited=$((waited + 2)) + done + echo " ⚠️ Сервис не отвечает после ${max_wait}s" + return 1 +} + +cleanup() { + rm -rf "$TMPDIR_STRESS" 2>/dev/null +} +trap cleanup EXIT + +echo "╔══════════════════════════════════════════════════════════════╗" +echo "║ shared-sqs STRESS TEST — $(date '+%Y-%m-%d %H:%M:%S') ║" +echo "║ Endpoint: ${BASE_URL} ║" +echo "║ Workers: ${CONCURRENT_WORKERS} Msgs/worker: ${MESSAGES_PER_WORKER} Burst: ${BURST_SIZE} ║" +echo "╚══════════════════════════════════════════════════════════════╝" +echo "" + +# ═══════════════════════════════════════════════════════════ +# 1. ПОДГОТОВКА — создаём тенантов и очереди +# ═══════════════════════════════════════════════════════════ +echo "═══ 1. Подготовка — тенанты и очереди ═══" + +# Создаём тенантов для теста +declare -a TENANT_AK +declare -a TENANT_SK +for i in $(seq 1 $TENANT_COUNT); do + R=$(admin_api POST "/admin/tenants" "{\"name\":\"stress-t${i}-${TS}\",\"access_key\":\"STRESSAK${i}${TS}\",\"secret_key\":\"STRESSSK${i}${TS}\"}") + TENANT_AK[$i]="STRESSAK${i}${TS}" + TENANT_SK[$i]="STRESSSK${i}${TS}" + echo " Тенант ${i}: AK=${TENANT_AK[$i]}" +done + +# Очередь для concurrency-теста +STRESS_Q="stress-concurrent-${TS}" +for i in $(seq 1 $TENANT_COUNT); do + R=$(sqs "${TENANT_AK[$i]}" "${TENANT_SK[$i]}" create-queue --queue-name "$STRESS_Q") +done +echo " Очередь: $STRESS_Q создана для $TENANT_COUNT тенантов" + +# Дополнительная очередь для burst-теста +BURST_Q="stress-burst-${TS}" +R=$(sqs "${TENANT_AK[1]}" "${TENANT_SK[1]}" create-queue --queue-name "$BURST_Q") +echo " Очередь: $BURST_Q (burst)" +echo "" + +# ═══════════════════════════════════════════════════════════ +# 2. КОНКУРЕНТНАЯ ОТПРАВКА — N воркеров пишут параллельно +# ═══════════════════════════════════════════════════════════ +echo "═══ 2. Конкурентная отправка: ${CONCURRENT_WORKERS} воркеров × ${MESSAGES_PER_WORKER} сообщений ═══" + +TOTAL_MSGS=$((CONCURRENT_WORKERS * MESSAGES_PER_WORKER)) +echo " Всего будет отправлено: $TOTAL_MSGS сообщений" + +# Запускаем воркеров параллельно +for w in $(seq 1 $CONCURRENT_WORKERS); do + ( + ok=0 + fail=0 + for m in $(seq 1 $MESSAGES_PER_WORKER); do + R=$(sqs "${TENANT_AK[1]}" "${TENANT_SK[1]}" send-message \ + --queue-url "${BASE_URL}/queue/${STRESS_Q}" \ + --message-body "worker-${w}-msg-${m}-${TS}" 2>&1) + if echo "$R" | grep -q "MessageId"; then + ok=$((ok + 1)) + else + fail=$((fail + 1)) + fi + done + echo "${ok}:${fail}" > "${TMPDIR_STRESS}/send_w${w}.result" + ) & +done +echo " ⏳ Ожидание завершения ${CONCURRENT_WORKERS} воркеров..." +wait + +# Подсчитываем результаты +SEND_OK=0 +SEND_FAIL=0 +for w in $(seq 1 $CONCURRENT_WORKERS); do + if [[ -f "${TMPDIR_STRESS}/send_w${w}.result" ]]; then + IFS=':' read -r ok fail < "${TMPDIR_STRESS}/send_w${w}.result" + SEND_OK=$((SEND_OK + ok)) + SEND_FAIL=$((SEND_FAIL + fail)) + fi +done +echo " Отправлено: ${SEND_OK} ok, ${SEND_FAIL} fail" +check_eq "Все сообщения отправлены" "$SEND_OK" "$TOTAL_MSGS" + +# Проверяем через GetQueueAttributes — ApproximateNumberOfMessages +sleep 2 +R=$(sqs "${TENANT_AK[1]}" "${TENANT_SK[1]}" get-queue-attributes \ + --queue-url "${BASE_URL}/queue/${STRESS_Q}" \ + --attribute-names ApproximateNumberOfMessages 2>&1) +APPROX=$(echo "$R" | grep -oP '"ApproximateNumberOfMessages"\s*:\s*"\K[0-9]+') +echo " Очередь сообщает: ~${APPROX} сообщений" +check_eq "Количество в очереди совпадает" "${APPROX:-0}" "$TOTAL_MSGS" +echo "" + +# ═══════════════════════════════════════════════════════════ +# 3. КОНКУРЕНТНОЕ ЧТЕНИЕ — гонка за сообщения +# ═══════════════════════════════════════════════════════════ +echo "═══ 3. Конкурентное чтение: ${CONCURRENT_WORKERS} воркеров забирают ═══" + +# Каждый воркер читает и удаляет +for w in $(seq 1 $CONCURRENT_WORKERS); do + ( + received=0 + deleted=0 + empty_rounds=0 + # Читаем пока есть сообщения (max 10 пустых подряд — выход) + while [[ $empty_rounds -lt 10 ]]; do + R=$(sqs "${TENANT_AK[1]}" "${TENANT_SK[1]}" receive-message \ + --queue-url "${BASE_URL}/queue/${STRESS_Q}" \ + --max-number-of-messages 10 \ + --wait-time-seconds 1 2>&1) + # Считаем полученные + MSG_COUNT=$(echo "$R" | grep -c '"MessageId"' || true) + if [[ "$MSG_COUNT" -eq 0 ]]; then + empty_rounds=$((empty_rounds + 1)) + continue + fi + empty_rounds=0 + received=$((received + MSG_COUNT)) + # Удаляем каждое полученное + HANDLES=$(echo "$R" | grep -oP '"ReceiptHandle"\s*:\s*"\K[^"]+') + while IFS= read -r handle; do + DR=$(sqs "${TENANT_AK[1]}" "${TENANT_SK[1]}" delete-message \ + --queue-url "${BASE_URL}/queue/${STRESS_Q}" \ + --receipt-handle "$handle" 2>&1) + deleted=$((deleted + 1)) + done <<< "$HANDLES" + done + echo "${received}:${deleted}" > "${TMPDIR_STRESS}/recv_w${w}.result" + ) & +done +echo " ⏳ Ожидание воркеров-читателей..." +wait + +# Подсчёт +RECV_TOTAL=0 +DEL_TOTAL=0 +for w in $(seq 1 $CONCURRENT_WORKERS); do + if [[ -f "${TMPDIR_STRESS}/recv_w${w}.result" ]]; then + IFS=':' read -r recv del < "${TMPDIR_STRESS}/recv_w${w}.result" + RECV_TOTAL=$((RECV_TOTAL + recv)) + DEL_TOTAL=$((DEL_TOTAL + del)) + fi +done +echo " Прочитано: ${RECV_TOTAL}, удалено: ${DEL_TOTAL}" +# Каждое сообщение может быть прочитано несколько раз (visibility timeout), +# но удалённых не должно быть больше отправленных +check_ge "Прочитано >= отправлено (с повторами ок)" "$RECV_TOTAL" "$TOTAL_MSGS" +check_eq "Удалено = отправлено (нет дубликатов)" "$DEL_TOTAL" "$TOTAL_MSGS" + +# Очередь должна быть пуста +sleep 2 +R=$(sqs "${TENANT_AK[1]}" "${TENANT_SK[1]}" get-queue-attributes \ + --queue-url "${BASE_URL}/queue/${STRESS_Q}" \ + --attribute-names ApproximateNumberOfMessages 2>&1) +APPROX=$(echo "$R" | grep -oP '"ApproximateNumberOfMessages"\s*:\s*"\K[0-9]+') +check_eq "Очередь пуста после чтения" "${APPROX:-0}" "0" +echo "" + +# ═══════════════════════════════════════════════════════════ +# 4. MULTI-TENANT ИЗОЛЯЦИЯ ПОД НАГРУЗКОЙ +# ═══════════════════════════════════════════════════════════ +echo "═══ 4. Multi-tenant изоляция: ${TENANT_COUNT} тенантов, параллельная нагрузка ═══" + +MSGS_PER_TENANT=30 +# Каждый тенант отправляет уникальные сообщения в одноимённую очередь +for i in $(seq 1 $TENANT_COUNT); do + ( + ok=0 + for m in $(seq 1 $MSGS_PER_TENANT); do + R=$(sqs "${TENANT_AK[$i]}" "${TENANT_SK[$i]}" send-message \ + --queue-url "${BASE_URL}/queue/${STRESS_Q}" \ + --message-body "tenant-${i}-isolation-${m}" 2>&1) + if echo "$R" | grep -q "MessageId"; then ok=$((ok + 1)); fi + done + echo "$ok" > "${TMPDIR_STRESS}/tenant_send_${i}.result" + ) & +done +wait + +# Каждый тенант читает — должен видеть ТОЛЬКО свои сообщения +for i in $(seq 1 $TENANT_COUNT); do + ( + own=0 + foreign=0 + empty_rounds=0 + while [[ $empty_rounds -lt 5 ]]; do + R=$(sqs "${TENANT_AK[$i]}" "${TENANT_SK[$i]}" receive-message \ + --queue-url "${BASE_URL}/queue/${STRESS_Q}" \ + --max-number-of-messages 10 \ + --wait-time-seconds 1 2>&1) + MSG_COUNT=$(echo "$R" | grep -c '"Body"' || true) + if [[ "$MSG_COUNT" -eq 0 ]]; then + empty_rounds=$((empty_rounds + 1)) + continue + fi + empty_rounds=0 + # Проверяем что тело содержит tenant-{i} + while IFS= read -r body; do + if echo "$body" | grep -q "tenant-${i}-"; then + own=$((own + 1)) + else + foreign=$((foreign + 1)) + fi + done < <(echo "$R" | grep -oP '"Body"\s*:\s*"\K[^"]+') + # Удаляем + HANDLES=$(echo "$R" | grep -oP '"ReceiptHandle"\s*:\s*"\K[^"]+') + while IFS= read -r handle; do + sqs "${TENANT_AK[$i]}" "${TENANT_SK[$i]}" delete-message \ + --queue-url "${BASE_URL}/queue/${STRESS_Q}" \ + --receipt-handle "$handle" >/dev/null 2>&1 + done <<< "$HANDLES" + done + echo "${own}:${foreign}" > "${TMPDIR_STRESS}/tenant_recv_${i}.result" + ) & +done +wait + +# Проверяем: чужих сообщений быть не должно +ALL_FOREIGN=0 +ALL_OWN=0 +for i in $(seq 1 $TENANT_COUNT); do + if [[ -f "${TMPDIR_STRESS}/tenant_recv_${i}.result" ]]; then + IFS=':' read -r own foreign < "${TMPDIR_STRESS}/tenant_recv_${i}.result" + echo " Тенант ${i}: своих=${own}, чужих=${foreign}" + ALL_OWN=$((ALL_OWN + own)) + ALL_FOREIGN=$((ALL_FOREIGN + foreign)) + fi +done +check_eq "Чужих сообщений: 0 (tenant isolation)" "$ALL_FOREIGN" "0" +check_ge "Свои сообщения получены" "$ALL_OWN" "$((TENANT_COUNT * MSGS_PER_TENANT))" +echo "" + +# ═══════════════════════════════════════════════════════════ +# 5. BURST — резкий всплеск запросов +# ═══════════════════════════════════════════════════════════ +echo "═══ 5. Burst: ${BURST_SIZE} сообщений одновременно ═══" + +# Отправляем BURST_SIZE сообщений максимально параллельно +for b in $(seq 1 $BURST_SIZE); do + ( + R=$(sqs "${TENANT_AK[1]}" "${TENANT_SK[1]}" send-message \ + --queue-url "${BASE_URL}/queue/${BURST_Q}" \ + --message-body "burst-${b}-${TS}" 2>&1) + if echo "$R" | grep -q "MessageId"; then echo "1"; else echo "0"; fi \ + > "${TMPDIR_STRESS}/burst_${b}.result" + ) & +done +echo " ⏳ Ожидание burst..." +wait + +BURST_OK=0 +for b in $(seq 1 $BURST_SIZE); do + if [[ -f "${TMPDIR_STRESS}/burst_${b}.result" ]]; then + val=$(cat "${TMPDIR_STRESS}/burst_${b}.result") + BURST_OK=$((BURST_OK + val)) + fi +done +echo " Burst: ${BURST_OK}/${BURST_SIZE} успешно" +check_ge "Burst: >= 90% успешно" "$BURST_OK" "$((BURST_SIZE * 90 / 100))" + +# Проверяем что очередь получила все +sleep 3 +R=$(sqs "${TENANT_AK[1]}" "${TENANT_SK[1]}" get-queue-attributes \ + --queue-url "${BASE_URL}/queue/${BURST_Q}" \ + --attribute-names ApproximateNumberOfMessages 2>&1) +BURST_APPROX=$(echo "$R" | grep -oP '"ApproximateNumberOfMessages"\s*:\s*"\K[0-9]+') +echo " Очередь burst: ~${BURST_APPROX} сообщений" +check_eq "Burst: все в очереди" "${BURST_APPROX:-0}" "$BURST_OK" +echo "" + +# ═══════════════════════════════════════════════════════════ +# 6. KILL POD — рестарт и восстановление из Redis +# ═══════════════════════════════════════════════════════════ +echo "═══ 6. Kill pod — рестарт и восстановление из Redis ═══" + +# Перед киллом — запоминаем сколько сообщений в burst-очереди +BEFORE_KILL="${BURST_APPROX:-0}" +echo " Сообщений в burst-очереди до kill: $BEFORE_KILL" + +# Отправляем ещё пачку — чтобы были "свежие" данные в Redis +EXTRA_MSGS=15 +for m in $(seq 1 $EXTRA_MSGS); do + sqs "${TENANT_AK[1]}" "${TENANT_SK[1]}" send-message \ + --queue-url "${BASE_URL}/queue/${BURST_Q}" \ + --message-body "pre-kill-${m}-${TS}" >/dev/null 2>&1 +done +EXPECTED_AFTER=$((BEFORE_KILL + EXTRA_MSGS)) +echo " Отправлено ещё $EXTRA_MSGS сообщений. Ожидаем: ${EXPECTED_AFTER}" + +# Убиваем под +echo " 🔪 Убиваю под..." +POD_NAME=$(kubectl get pods -n "$NAMESPACE" -l app.kubernetes.io/name=shared-sqs \ + -o jsonpath='{.items[0].metadata.name}' 2>/dev/null) +echo " Pod: $POD_NAME" +kubectl delete pod "$POD_NAME" -n "$NAMESPACE" --grace-period=0 --force 2>&1 | head -2 +echo " Под удалён, ждём рестарт..." + +# Ждём пока новый под поднимется +wait_for_pod_ready +wait_for_healthy + +# Проверяем что burst-очередь восстановилась из Redis +sleep 3 +R=$(sqs "${TENANT_AK[1]}" "${TENANT_SK[1]}" get-queue-attributes \ + --queue-url "${BASE_URL}/queue/${BURST_Q}" \ + --attribute-names ApproximateNumberOfMessages 2>&1) +AFTER_KILL=$(echo "$R" | grep -oP '"ApproximateNumberOfMessages"\s*:\s*"\K[0-9]+') +echo " Сообщений после рестарта: ${AFTER_KILL:-0}" +check_eq "Данные восстановлены из Redis" "${AFTER_KILL:-0}" "$EXPECTED_AFTER" + +# Проверяем что тенанты тоже восстановились +R=$(admin_api GET "/admin/tenants") +TENANT_CHECK=$(echo "$R" | grep -c "stress-t1-${TS}" || true) +check_ge "Тенанты восстановлены из Redis" "$TENANT_CHECK" "1" +echo "" + +# ═══════════════════════════════════════════════════════════ +# 7. REDIS DISCONNECT — сервис при потере Redis +# ═══════════════════════════════════════════════════════════ +echo "═══ 7. Redis disconnect simulation ═══" + +# Находим Redis endpoint из env пода +NEW_POD=$(kubectl get pods -n "$NAMESPACE" -l app.kubernetes.io/name=shared-sqs \ + -o jsonpath='{.items[0].metadata.name}' 2>/dev/null) + +# Добавляем NetworkPolicy чтобы заблокировать трафик к Redis +cat <<'NETPOL' | kubectl apply -n "$NAMESPACE" -f - 2>&1 +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: stress-test-block-redis +spec: + podSelector: + matchLabels: + app.kubernetes.io/name: shared-sqs + policyTypes: + - Egress + egress: + - to: + - ipBlock: + cidr: 0.0.0.0/0 + except: + - 10.0.0.0/8 + ports: + - protocol: TCP + port: 443 + - protocol: TCP + port: 80 + - to: + - namespaceSelector: {} + ports: + - protocol: TCP + port: 53 + - protocol: UDP + port: 53 +NETPOL +echo " NetworkPolicy: заблокирован egress к Redis (10.0.0.0/8 except DNS)" +sleep 5 + +# Пробуем API — должен вернуть ошибку или graceful degradation, не 502 +R_CODE=$(curl -s -o /dev/null -w "%{http_code}" --max-time 10 \ + -H "Authorization: Bearer $ADMIN_TOKEN" \ + "${BASE_URL}/admin/tenants" 2>/dev/null) +echo " HTTP status при потере Redis: $R_CODE" +TOTAL=$((TOTAL + 1)) +# Сервис может вернуть 200 (из кеша) или 500/503 (graceful), но НЕ пустоту/таймаут +if [[ "$R_CODE" =~ ^[2345][0-9][0-9]$ ]]; then + PASS=$((PASS + 1)) + echo " ✅ Сервис отвечает при потере Redis (HTTP $R_CODE)" +else + FAIL=$((FAIL + 1)) + echo " ❌ Сервис не отвечает при потере Redis (HTTP $R_CODE)" +fi + +# Восстанавливаем доступ к Redis — удаляем NetworkPolicy +kubectl delete networkpolicy stress-test-block-redis -n "$NAMESPACE" 2>&1 | head -1 +echo " NetworkPolicy удалена — Redis доступен снова" +sleep 5 + +# Проверяем что после восстановления Redis сервис работает +R=$(sqs "${TENANT_AK[1]}" "${TENANT_SK[1]}" send-message \ + --queue-url "${BASE_URL}/queue/${BURST_Q}" \ + --message-body "after-redis-recovery-${TS}" 2>&1) +check "Сервис работает после восстановления Redis" "$R" "MessageId" +echo "" + +# ═══════════════════════════════════════════════════════════ +# 8. ПАРАЛЛЕЛЬНЫЕ ОПЕРАЦИИ НА ОДНОЙ ОЧЕРЕДИ (смешанная нагрузка) +# ═══════════════════════════════════════════════════════════ +echo "═══ 8. Смешанная нагрузка: send + receive + delete одновременно ═══" + +MIXED_Q="stress-mixed-${TS}" +R=$(sqs "${TENANT_AK[1]}" "${TENANT_SK[1]}" create-queue --queue-name "$MIXED_Q") + +# Заполняем 50 сообщений +for m in $(seq 1 50); do + sqs "${TENANT_AK[1]}" "${TENANT_SK[1]}" send-message \ + --queue-url "${BASE_URL}/queue/${MIXED_Q}" \ + --message-body "mixed-init-${m}" >/dev/null 2>&1 +done +echo " Начальная загрузка: 50 сообщений" + +# Параллельно: 5 отправителей + 5 читателей-удалителей + 2 GetQueueAttributes +MIXED_DURATION=15 # секунды + +# Отправители +for w in $(seq 1 5); do + ( + end=$((SECONDS + MIXED_DURATION)) + ok=0 + while [[ $SECONDS -lt $end ]]; do + R=$(sqs "${TENANT_AK[1]}" "${TENANT_SK[1]}" send-message \ + --queue-url "${BASE_URL}/queue/${MIXED_Q}" \ + --message-body "mixed-send-${w}-$(date +%s%N)" 2>&1) + if echo "$R" | grep -q "MessageId"; then ok=$((ok + 1)); fi + done + echo "$ok" > "${TMPDIR_STRESS}/mixed_send_${w}.result" + ) & +done + +# Читатели-удалители +for w in $(seq 1 5); do + ( + end=$((SECONDS + MIXED_DURATION)) + recv=0 + while [[ $SECONDS -lt $end ]]; do + R=$(sqs "${TENANT_AK[1]}" "${TENANT_SK[1]}" receive-message \ + --queue-url "${BASE_URL}/queue/${MIXED_Q}" \ + --max-number-of-messages 10 --wait-time-seconds 1 2>&1) + HANDLES=$(echo "$R" | grep -oP '"ReceiptHandle"\s*:\s*"\K[^"]+') + while IFS= read -r handle; do + [[ -z "$handle" ]] && continue + sqs "${TENANT_AK[1]}" "${TENANT_SK[1]}" delete-message \ + --queue-url "${BASE_URL}/queue/${MIXED_Q}" \ + --receipt-handle "$handle" >/dev/null 2>&1 + recv=$((recv + 1)) + done <<< "$HANDLES" + done + echo "$recv" > "${TMPDIR_STRESS}/mixed_recv_${w}.result" + ) & +done + +# GetQueueAttributes (не должен падать при нагрузке) +for w in $(seq 1 2); do + ( + end=$((SECONDS + MIXED_DURATION)) + ok=0 + fail=0 + while [[ $SECONDS -lt $end ]]; do + R=$(sqs "${TENANT_AK[1]}" "${TENANT_SK[1]}" get-queue-attributes \ + --queue-url "${BASE_URL}/queue/${MIXED_Q}" \ + --attribute-names All 2>&1) + if echo "$R" | grep -q "ApproximateNumberOfMessages"; then + ok=$((ok + 1)) + else + fail=$((fail + 1)) + fi + sleep 0.5 + done + echo "${ok}:${fail}" > "${TMPDIR_STRESS}/mixed_attr_${w}.result" + ) & +done + +echo " ⏳ Смешанная нагрузка ${MIXED_DURATION}s..." +wait + +# Подсчёт +MIXED_SENT=0 +MIXED_RECV=0 +ATTR_OK=0 +ATTR_FAIL=0 +for w in $(seq 1 5); do + [[ -f "${TMPDIR_STRESS}/mixed_send_${w}.result" ]] && MIXED_SENT=$((MIXED_SENT + $(cat "${TMPDIR_STRESS}/mixed_send_${w}.result"))) + [[ -f "${TMPDIR_STRESS}/mixed_recv_${w}.result" ]] && MIXED_RECV=$((MIXED_RECV + $(cat "${TMPDIR_STRESS}/mixed_recv_${w}.result"))) +done +for w in $(seq 1 2); do + if [[ -f "${TMPDIR_STRESS}/mixed_attr_${w}.result" ]]; then + IFS=':' read -r ok fail < "${TMPDIR_STRESS}/mixed_attr_${w}.result" + ATTR_OK=$((ATTR_OK + ok)) + ATTR_FAIL=$((ATTR_FAIL + fail)) + fi +done +echo " Отправлено: $MIXED_SENT, прочитано+удалено: $MIXED_RECV" +echo " GetQueueAttributes: ok=$ATTR_OK, fail=$ATTR_FAIL" + +check_ge "Смешанная нагрузка: отправлено > 0" "$MIXED_SENT" "1" +check_ge "Смешанная нагрузка: прочитано > 0" "$MIXED_RECV" "1" +check_eq "GetQueueAttributes: ни одного сбоя" "$ATTR_FAIL" "0" +echo "" + +# ═══════════════════════════════════════════════════════════ +# 9. CLEANUP +# ═══════════════════════════════════════════════════════════ +echo "═══ 9. Cleanup ═══" + +for i in $(seq 1 $TENANT_COUNT); do + sqs "${TENANT_AK[$i]}" "${TENANT_SK[$i]}" delete-queue \ + --queue-url "${BASE_URL}/queue/${STRESS_Q}" >/dev/null 2>&1 +done +sqs "${TENANT_AK[1]}" "${TENANT_SK[1]}" delete-queue \ + --queue-url "${BASE_URL}/queue/${BURST_Q}" >/dev/null 2>&1 +sqs "${TENANT_AK[1]}" "${TENANT_SK[1]}" delete-queue \ + --queue-url "${BASE_URL}/queue/${MIXED_Q}" >/dev/null 2>&1 +echo " Очереди и тенанты очищены" +echo "" + +# ═══════════════════════════════════════════════════════════ +# ИТОГИ +# ═══════════════════════════════════════════════════════════ +echo "╔══════════════════════════════════════════════════════════════╗" +echo "║ ИТОГО: ${PASS}/${TOTAL} ✅ ${FAIL}/${TOTAL} ❌ ║" +echo "╚══════════════════════════════════════════════════════════════╝" + +if [[ $FAIL -gt 0 ]]; then + exit 1 +fi