Files
SQS-service/tests/stress_test.sh
T

657 lines
29 KiB
Bash
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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