diff --git a/compare_sqs.sh b/compare_sqs.sh new file mode 100644 index 0000000..e7eddc5 --- /dev/null +++ b/compare_sqs.sh @@ -0,0 +1,552 @@ +#!/bin/bash +# compare_sqs.sh — Сравнительный тест двух SQS реализаций +# Created: 2026-04-10 +# +# Сравниваем: +# A) sqs-operator — https://sqs.kube5s.ru/sqs/{tenant} (1 тенант = 1 namespace в k8s) +# B) shared-sqs — https://qu.kube5s.ru (Single-process, multitenancy через API) +# +# Метрики: +# - Время создания тенанта +# - Время CRUD очереди (create/send/receive/delete) +# - Время удаления тенанта +# - Ресурсы pods после N тенантов (kubectl top) +# +# Запуск: +# bash compare_sqs.sh [TENANTS] +# TENANTS=10 bash compare_sqs.sh +# TENANTS=20 bash compare_sqs.sh +# TENANTS=50 bash compare_sqs.sh +# +# Требования: +# - kubectl с доступом к кластеру +# - curl +# - python3 +# - aws CLI (для shared-sqs CRUD) +# +# Для sqs-operator: тенант создаётся через kubectl apply QueueService CR +# Для shared-sqs: тенант создаётся через POST /admin/tenants + +set -uo pipefail + +# ════════════════════════════════════════════ +# КОНФИГ +# ════════════════════════════════════════════ + +# Количество тенантов — передаётся аргументом или переменной или по умолчанию 10 +TENANTS="${1:-${TENANTS:-10}}" + +# sqs-operator +OP_HOST="https://sqs.kube5s.ru" +OP_CR_NAMESPACE="sqs-operator-system" +# Namespace тенантов в sqs-operator: sless-fn-{tenant} +OP_TENANT_NS_PREFIX="sless-fn" +# Шаблон QueueService CR (apiVersion из реального кода) +OP_CR_TEMPLATE='apiVersion: sqs.kube5s.ru/v1alpha1 +kind: QueueService +metadata: + name: CRNAME + namespace: sqs-operator-system +spec: + tenantId: TENANTID + memoryMB: 64 + storageMB: 512 + persistence: false' + +# shared-sqs +SS_HOST="https://qu.kube5s.ru" +SS_ADMIN_TOKEN="${SS_ADMIN_TOKEN:-sqs-admin-7a7d8bd0c060a75c198d48680f34077a}" +SS_REGION="us-east-1" +SS_NAMESPACE="sless" # namespace где живёт shared-sqs pod (для kubectl top) + +# Имя пода shared-sqs в k8s (для ресурсов) +SS_DEPLOY_LABEL="app=shared-sqs" +OP_MANAGER_LABEL="control-plane=controller-manager" +OP_TENANT_LABEL_PREFIX="sless.dev/tenant" + +# Таймаут ожидания QueueService Ready (секунд) +OP_READY_TIMEOUT=120 + +# ════════════════════════════════════════════ +# УТИЛИТЫ +# ════════════════════════════════════════════ + +PASS=0; FAIL=0 +RESULTS_FILE="/tmp/sqs_compare_$(date +%s).log" + +ok() { echo " ✅ $1"; PASS=$((PASS+1)); } +fail() { echo " ❌ $1"; FAIL=$((FAIL+1)); } +hdr() { echo ""; echo "══════════════════════════════════════════"; echo " $1"; echo "══════════════════════════════════════════"; } +ts() { date '+%H:%M:%S'; } +elapsed_ms() { + # $1 = start seconds (с дробной частью от date +%s%3N) + local start="$1" + local end + end=$(date +%s%3N) + echo $(( end - start )) +} + +# Запись результата в файл для итоговой таблицы +record() { + # $1=impl $2=phase $3=n_tenants $4=value_ms $5=label + echo "$1,$2,$3,$4,$5" >> "$RESULTS_FILE" +} + +# ─── sqs-operator API ─── +op_sqs() { + # $1=endpoint $2=ak $3=sk $4=query_params + curl -sk --max-time 15 \ + --aws-sigv4 "aws:amz:us-east-1:sqs" \ + --user "$2:$3" \ + "${1}/?${4}&Version=2012-11-05" +} + +# Ожидать QueueService Phase=Ready +op_wait_ready() { + local crname="$1" max_sec="$2" + local start + start=$(date +%s) + while true; do + local phase + phase=$(kubectl get queueservice "$crname" -n "$OP_CR_NAMESPACE" \ + -o jsonpath='{.status.phase}' 2>/dev/null || echo "") + [ "$phase" = "Ready" ] && return 0 + local now + now=$(date +%s) + [ $(( now - start )) -ge "$max_sec" ] && return 1 + sleep 2 + done +} + +# Проверить что QueueService CR API доступен (идемпотентно) +op_check_api() { + local tenantid="$1" + local ep="${OP_HOST}/sqs/${tenantid}" + # Для credentials нужен secret в k8s + local ns="${OP_TENANT_NS_PREFIX}-${tenantid}" + local ak sk + ak=$(kubectl -n "$ns" get secret "sqs-creds-${tenantid}" \ + -o jsonpath='{.data.accessKey}' 2>/dev/null | base64 -d 2>/dev/null || echo "") + sk=$(kubectl -n "$ns" get secret "sqs-creds-${tenantid}" \ + -o jsonpath='{.data.secretKey}' 2>/dev/null | base64 -d 2>/dev/null || echo "") + echo "$ak:$sk:$ep" +} + +# ─── shared-sqs Admin API ─── +ss_admin() { + local method="$1" path="$2" body="${3:-}" + if [[ -n "$body" ]]; then + curl -sf --max-time 15 \ + -X "$method" \ + -H "Authorization: Bearer $SS_ADMIN_TOKEN" \ + -H "Content-Type: application/json" \ + -d "$body" \ + "${SS_HOST}${path}" 2>&1 + else + curl -sf --max-time 15 \ + -X "$method" \ + -H "Authorization: Bearer $SS_ADMIN_TOKEN" \ + "${SS_HOST}${path}" 2>&1 + fi +} + +ss_sqs() { + # $1=ak $2=sk $3=action_params + curl -sk --max-time 15 \ + -u "$1:$2" \ + "${SS_HOST}/?${3}&Version=2012-11-05" 2>&1 +} + +# ════════════════════════════════════════════ +# ПРОВЕРКА ДОСТУПНОСТИ +# ════════════════════════════════════════════ +hdr "0. Проверка доступности (N=$TENANTS тенантов)" + +OP_HEALTH=$(curl -sk --max-time 5 -o /dev/null -w "%{http_code}" "${OP_HOST}/sqs/test001" 2>/dev/null || echo "0") +SS_HEALTH=$(curl -sk --max-time 5 -o /dev/null -w "%{http_code}" "${SS_HOST}/health" 2>/dev/null || echo "0") + +echo " sqs-operator (${OP_HOST}): HTTP $OP_HEALTH" +echo " shared-sqs (${SS_HOST}): HTTP $SS_HEALTH" + +# kubectl доступен? +KUBECTL_OK=false +if kubectl cluster-info &>/dev/null; then + KUBECTL_OK=true + echo " kubectl: OK ($(kubectl config current-context))" +else + echo " kubectl: НЕДОСТУПЕН — метрики ресурсов будут пропущены" +fi + +# aws cli доступен? +AWS_CLI_OK=false +if command -v aws &>/dev/null; then + AWS_CLI_OK=true + echo " aws CLI: OK" +else + echo " aws CLI: НЕДОСТУПЕН — shared-sqs CRUD будет через curl" +fi + +# ════════════════════════════════════════════ +# ФАЗА 1 — Создание N тенантов +# ════════════════════════════════════════════ +hdr "1. Создание $TENANTS тенантов" + +# Массивы для хранения созданных тенантов +declare -a OP_TENANTS=() # tenantId +declare -a OP_CR_NAMES=() # имя CR +declare -a SS_TENANT_IDS=() # UUID тенанта +declare -a SS_AK_LIST=() # Access Keys +declare -a SS_SK_LIST=() # Secret Keys + +echo "" +echo " ── A) sqs-operator ──" +echo " (каждый тенант = QueueService CR → новый namespace + deployment + secret)" +echo "" + +OP_TOTAL_CREATE_MS=0 +OP_CREATE_TIMES=() + +for i in $(seq 1 "$TENANTS"); do + tid="cmp-op-$(printf '%03d' $i)-$$" + crname="cmp-cr-$(printf '%03d' $i)-$$" + + # Применяем CR + CR_YAML=$(echo "$OP_CR_TEMPLATE" | sed "s/CRNAME/$crname/g" | sed "s/TENANTID/$tid/g") + t_start=$(date +%s%3N) + echo "$CR_YAML" | kubectl apply -f - &>/dev/null + apply_ok=$? + + if [[ $apply_ok -ne 0 ]]; then + echo " ❌ Тенант #$i ($tid): kubectl apply failed" + continue + fi + + # Ждём Ready + if op_wait_ready "$crname" "$OP_READY_TIMEOUT"; then + t_ms=$(elapsed_ms "$t_start") + OP_CREATE_TIMES+=("$t_ms") + OP_TOTAL_CREATE_MS=$(( OP_TOTAL_CREATE_MS + t_ms )) + printf " ✅ #%-3d %-40s %dms\n" "$i" "$tid" "$t_ms" + record "sqs-operator" "tenant_create" "$i" "$t_ms" "$tid" + else + echo " ⚠️ #$i ($tid): timeout — не стал Ready за ${OP_READY_TIMEOUT}s" + record "sqs-operator" "tenant_create_timeout" "$i" "$OP_READY_TIMEOUT\000" "$tid" + fi + + OP_TENANTS+=("$tid") + OP_CR_NAMES+=("$crname") +done + +OP_CREATED=${#OP_TENANTS[@]} +if [[ $OP_CREATED -gt 0 ]]; then + OP_AVG_CREATE=$(( OP_TOTAL_CREATE_MS / OP_CREATED )) + # Медиана + OP_SORTED_TIMES=($(printf '%s\n' "${OP_CREATE_TIMES[@]}" | sort -n)) + OP_MEDIAN_CREATE=${OP_SORTED_TIMES[$(( OP_CREATED / 2 ))]} + printf "\n Итого sqs-operator: создано %d/%d avg=%dms median=%dms\n" \ + "$OP_CREATED" "$TENANTS" "$OP_AVG_CREATE" "$OP_MEDIAN_CREATE" +fi + +echo "" +echo " ── B) shared-sqs ──" +echo " (тенант = POST /admin/tenants, всё в памяти одного процесса)" +echo "" + +SS_TOTAL_CREATE_MS=0 +SS_CREATE_TIMES=() + +for i in $(seq 1 "$TENANTS"); do + tname="cmp-ss-$(printf '%03d' $i)-$$" + t_start=$(date +%s%3N) + + RESP=$(ss_admin POST /admin/tenants "{\"name\":\"${tname}\",\"max_queues\":50}" 2>&1) + t_ms=$(elapsed_ms "$t_start") + + if echo "$RESP" | grep -q "access_key"; then + ak=$(echo "$RESP" | python3 -c "import sys,json; print(json.load(sys.stdin)['access_key'])" 2>/dev/null || echo "") + sk=$(echo "$RESP" | python3 -c "import sys,json; print(json.load(sys.stdin)['secret_key'])" 2>/dev/null || echo "") + tid=$(echo "$RESP" | python3 -c "import sys,json; print(json.load(sys.stdin)['id'])" 2>/dev/null || echo "") + + SS_CREATE_TIMES+=("$t_ms") + SS_TOTAL_CREATE_MS=$(( SS_TOTAL_CREATE_MS + t_ms )) + printf " ✅ #%-3d %-40s %dms (id=%s)\n" "$i" "$tname" "$t_ms" "${tid:0:12}..." + record "shared-sqs" "tenant_create" "$i" "$t_ms" "$tname" + + SS_TENANT_IDS+=("$tid") + SS_AK_LIST+=("$ak") + SS_SK_LIST+=("$sk") + else + echo " ❌ #$i ($tname): ошибка: ${RESP:0:100}" + record "shared-sqs" "tenant_create_error" "$i" "0" "$tname" + fi +done + +SS_CREATED=${#SS_TENANT_IDS[@]} +if [[ $SS_CREATED -gt 0 ]]; then + SS_AVG_CREATE=$(( SS_TOTAL_CREATE_MS / SS_CREATED )) + SS_SORTED_TIMES=($(printf '%s\n' "${SS_CREATE_TIMES[@]}" | sort -n)) + SS_MEDIAN_CREATE=${SS_SORTED_TIMES[$(( SS_CREATED / 2 ))]} + printf "\n Итого shared-sqs: создано %d/%d avg=%dms median=%dms\n" \ + "$SS_CREATED" "$TENANTS" "$SS_AVG_CREATE" "$SS_MEDIAN_CREATE" +fi + +# ════════════════════════════════════════════ +# ФАЗА 2 — CRUD очереди (первые 3 тенанта) +# ════════════════════════════════════════════ +hdr "2. CRUD очереди (send+receive+delete, первые 3 тенанта каждой реализации)" + +CRUD_LIMIT=3 + +echo "" +echo " ── A) sqs-operator CRUD ──" +echo "" + +OP_CRUD_TIMES=() +for idx in $(seq 0 $(( CRUD_LIMIT - 1 ))); do + [[ $idx -ge ${#OP_TENANTS[@]} ]] && break + tid="${OP_TENANTS[$idx]}" + crname="${OP_CR_NAMES[$idx]}" + creds=$(op_check_api "$tid") + ak=$(echo "$creds" | cut -d: -f1) + sk=$(echo "$creds" | cut -d: -f2) + ep=$(echo "$creds" | cut -d: -f3-) + + if [[ -z "$ak" || -z "$sk" ]]; then + echo " ⚠️ #$idx skipped — нет credentials (тенант не Ready?)" + continue + fi + + qname="crud-q-$$-$idx" + t_start=$(date +%s%3N) + + # CreateQueue + R=$(curl -sk --max-time 15 --aws-sigv4 "aws:amz:us-east-1:sqs" --user "$ak:$sk" \ + "${ep}/?Action=CreateQueue&QueueName=${qname}&Version=2012-11-05") + QURL=$(echo "$R" | grep -oP "(?<=)[^<]+" || echo "") + + if [[ -z "$QURL" ]]; then + echo " ❌ #$idx CreateQueue failed: ${R:0:80}" + continue + fi + + # SendMessage + curl -sk --max-time 15 --aws-sigv4 "aws:amz:us-east-1:sqs" --user "$ak:$sk" \ + "${ep}/?Action=SendMessage&QueueUrl=${QURL}&MessageBody=compare-test-${idx}&Version=2012-11-05" >/dev/null + + # ReceiveMessage + R=$(curl -sk --max-time 15 --aws-sigv4 "aws:amz:us-east-1:sqs" --user "$ak:$sk" \ + "${ep}/?Action=ReceiveMessage&QueueUrl=${QURL}&MaxNumberOfMessages=1&Version=2012-11-05") + RECEIPT=$(echo "$R" | grep -oP "(?<=)[^<]+" | head -1 || echo "") + + # DeleteMessage + if [[ -n "$RECEIPT" ]]; then + RECEIPT_ENC=$(echo "$RECEIPT" | python3 -c "import sys,urllib.parse; print(urllib.parse.quote(sys.stdin.read().strip()))") + curl -sk --max-time 15 --aws-sigv4 "aws:amz:us-east-1:sqs" --user "$ak:$sk" \ + "${ep}/?Action=DeleteMessage&QueueUrl=${QURL}&ReceiptHandle=${RECEIPT_ENC}&Version=2012-11-05" >/dev/null + fi + + # DeleteQueue + curl -sk --max-time 15 --aws-sigv4 "aws:amz:us-east-1:sqs" --user "$ak:$sk" \ + "${ep}/?Action=DeleteQueue&QueueUrl=${QURL}&Version=2012-11-05" >/dev/null + + t_ms=$(elapsed_ms "$t_start") + OP_CRUD_TIMES+=("$t_ms") + printf " ✅ Tenant #%d %-36s CRUD %dms\n" "$idx" "$tid" "$t_ms" + record "sqs-operator" "crud" "$idx" "$t_ms" "$tid" +done + +echo "" +echo " ── B) shared-sqs CRUD ──" +echo "" + +SS_CRUD_TIMES=() +for idx in $(seq 0 $(( CRUD_LIMIT - 1 ))); do + [[ $idx -ge ${#SS_TENANT_IDS[@]} ]] && break + tid="${SS_TENANT_IDS[$idx]}" + ak="${SS_AK_LIST[$idx]}" + sk="${SS_SK_LIST[$idx]}" + qname="crud-q-$$-$idx" + + t_start=$(date +%s%3N) + + # CreateQueue — shared-sqs требует AWS SigV4, не Basic Auth + R=$(curl -sk --max-time 15 \ + --aws-sigv4 "aws:amz:us-east-1:sqs" --user "$ak:$sk" \ + "${SS_HOST}/?Action=CreateQueue&QueueName=${qname}&Version=2012-11-05") + QURL=$(echo "$R" | grep -oP "(?<=)[^<]+" || echo "") + + if [[ -z "$QURL" ]]; then + echo " ❌ #$idx CreateQueue failed: ${R:0:80}" + continue + fi + + # SendMessage + curl -sk --max-time 15 \ + --aws-sigv4 "aws:amz:us-east-1:sqs" --user "$ak:$sk" \ + "${SS_HOST}/?Action=SendMessage&QueueUrl=${QURL}&MessageBody=compare-test-${idx}&Version=2012-11-05" >/dev/null + + # ReceiveMessage + R=$(curl -sk --max-time 15 \ + --aws-sigv4 "aws:amz:us-east-1:sqs" --user "$ak:$sk" \ + "${SS_HOST}/?Action=ReceiveMessage&QueueUrl=${QURL}&MaxNumberOfMessages=1&Version=2012-11-05") + RECEIPT=$(echo "$R" | grep -oP "(?<=)[^<]+" | head -1 || echo "") + + # DeleteMessage + if [[ -n "$RECEIPT" ]]; then + RECEIPT_ENC=$(echo "$RECEIPT" | python3 -c "import sys,urllib.parse; print(urllib.parse.quote(sys.stdin.read().strip()))") + curl -sk --max-time 15 \ + --aws-sigv4 "aws:amz:us-east-1:sqs" --user "$ak:$sk" \ + "${SS_HOST}/?Action=DeleteMessage&QueueUrl=${QURL}&ReceiptHandle=${RECEIPT_ENC}&Version=2012-11-05" >/dev/null + fi + + # DeleteQueue + curl -sk --max-time 15 \ + --aws-sigv4 "aws:amz:us-east-1:sqs" --user "$ak:$sk" \ + "${SS_HOST}/?Action=DeleteQueue&QueueUrl=${QURL}&Version=2012-11-05" >/dev/null + + t_ms=$(elapsed_ms "$t_start") + SS_CRUD_TIMES+=("$t_ms") + printf " ✅ Tenant #%d %-36s CRUD %dms\n" "$idx" "${tid:0:20}..." "$t_ms" + record "shared-sqs" "crud" "$idx" "$t_ms" "${tid:0:12}" +done + +# ════════════════════════════════════════════ +# ФАЗА 3 — Ресурсы (kubectl top) +# ════════════════════════════════════════════ +hdr "3. Ресурсы при $TENANTS тенантах (kubectl top)" + +if [[ "$KUBECTL_OK" == "true" ]]; then + echo "" + echo " ── A) sqs-operator pods ──" + # Controller manager + echo " -- Controller manager:" + kubectl top pods -n "$OP_CR_NAMESPACE" -l "$OP_MANAGER_LABEL" 2>/dev/null || echo " (kubectl top недоступен — metrics-server?)" + # Tenant pods — все ns с prefix sless-fn-cmp- + echo " -- Tenant pods (cmp-* namespaces):" + for tid in "${OP_TENANTS[@]}"; do + ns="${OP_TENANT_NS_PREFIX}-${tid}" + kubectl top pods -n "$ns" 2>/dev/null | grep -v "^NAME" | \ + awk -v ns="$ns" '{printf " %-50s CPU=%-8s MEM=%s\n", ns"/"$1, $2, $3}' 2>/dev/null || true + done + # Суммарно: кол-во pods + OP_NS_COUNT=$(kubectl get ns | grep -c "sless-fn-cmp-" 2>/dev/null || echo "?") + echo " -- Итого namespace под тенанты: $OP_NS_COUNT" + + echo "" + echo " ── B) shared-sqs pod ──" + kubectl top pods -n "$SS_NAMESPACE" -l "$SS_DEPLOY_LABEL" 2>/dev/null || \ + kubectl top pods -n "$SS_NAMESPACE" 2>/dev/null | grep -i "shared-sqs\|sqs" | \ + awk '{printf " %-50s CPU=%-8s MEM=%s\n", $1, $2, $3}' || \ + echo " (pod не найден в namespace $SS_NAMESPACE)" + echo " -- Тенанты в памяти: $SS_CREATED (0 дополнительных pods)" +else + echo " kubectl недоступен — пропускаем" +fi + +# ════════════════════════════════════════════ +# ФАЗА 4 — Cleanup +# ════════════════════════════════════════════ +hdr "4. Cleanup" + +echo "" +echo " ── A) sqs-operator ──" +OP_DEL_TIMES=() +for i in "${!OP_CR_NAMES[@]}"; do + crname="${OP_CR_NAMES[$i]}" + t_start=$(date +%s%3N) + kubectl delete queueservice "$crname" -n "$OP_CR_NAMESPACE" --wait=false &>/dev/null + t_ms=$(elapsed_ms "$t_start") + OP_DEL_TIMES+=("$t_ms") + record "sqs-operator" "tenant_delete" "$i" "$t_ms" "$crname" +done +echo " Удалено ${#OP_CR_NAMES[@]} CR (без ожидания термин.)" + +echo "" +echo " ── B) shared-sqs ──" +SS_DEL_TIMES=() +for i in "${!SS_TENANT_IDS[@]}"; do + tid="${SS_TENANT_IDS[$i]}" + t_start=$(date +%s%3N) + ss_admin DELETE "/admin/tenants/$tid" >/dev/null 2>&1 + t_ms=$(elapsed_ms "$t_start") + SS_DEL_TIMES+=("$t_ms") + record "shared-sqs" "tenant_delete" "$i" "$t_ms" "${tid:0:12}" +done + +if [[ ${#SS_DEL_TIMES[@]} -gt 0 ]]; then + SS_AVG_DEL=$(python3 -c "t=[${SS_DEL_TIMES[*]}]; print(int(sum(t)/len(t)))" 2>/dev/null || echo "?") + echo " Удалено ${#SS_DEL_TIMES[@]} тенантов avg=${SS_AVG_DEL}ms" +fi + +# ════════════════════════════════════════════ +# ИТОГОВАЯ ТАБЛИЦА +# ════════════════════════════════════════════ +hdr "ИТОГ — Сравнительная таблица (N=$TENANTS)" + +python3 - << PYEOF +import csv, sys + +data = {} + +# Парсим results file +try: + with open("$RESULTS_FILE") as f: + for row in csv.reader(f): + if len(row) < 5: + continue + impl, phase, n, val_str, label = row + try: + val = int(val_str) + except: + continue + key = (impl, phase) + data.setdefault(key, []).append(val) +except FileNotFoundError: + print("Нет результатов") + sys.exit(0) + +def stats(vals): + if not vals: + return "нет данных" + vals_s = sorted(vals) + n = len(vals_s) + avg = int(sum(vals_s) / n) + med = vals_s[n // 2] + mn = vals_s[0] + mx = vals_s[-1] + return f"avg={avg}ms med={med}ms min={mn}ms max={mx}ms n={n}" + +print("") +print(f"{'Метрика':<30} {'sqs-operator':<50} {'shared-sqs':<50}") +print("-" * 130) + +phases = [ + ("tenant_create", "Создание тенанта"), + ("crud", "CRUD очереди (create+send+recv+del)"), + ("tenant_delete", "Удаление тенанта"), +] + +for phase_key, phase_label in phases: + op_vals = data.get(("sqs-operator", phase_key), []) + ss_vals = data.get(("shared-sqs", phase_key), []) + print(f" {phase_label:<28} {stats(op_vals):<50} {stats(ss_vals):<50}") + +print("") + +# Вывод победителя по скорости создания +op_create = data.get(("sqs-operator", "tenant_create"), []) +ss_create = data.get(("shared-sqs", "tenant_create"), []) +if op_create and ss_create: + op_avg = sum(op_create) / len(op_create) + ss_avg = sum(ss_create) / len(ss_create) + ratio = op_avg / ss_avg if ss_avg > 0 else float('inf') + faster = "shared-sqs" if ss_avg < op_avg else "sqs-operator" + print(f" Создание тенанта: {faster} быстрее в {ratio:.1f}x") + print(f" sqs-operator: {op_avg:.0f}ms | shared-sqs: {ss_avg:.0f}ms") + +print("") +print(f" Файл подробных данных: $RESULTS_FILE") +PYEOF + +echo "" +echo " Готово! $(date '+%Y-%m-%d %H:%M:%S')" diff --git a/doc/models/agent_summary.md b/doc/models/agent_summary.md new file mode 100644 index 0000000..bc277b1 --- /dev/null +++ b/doc/models/agent_summary.md @@ -0,0 +1,16 @@ +# Сводка агентов (моделей) + +Дата: 2026-04-13 + +| Агент | Контекст | Цена | Главная фишка | +|-------|---------:|:----:|---------------| +| GPT-5.4 | 400K | 1x | «Видит» всё. Лидер по объему знаний. | +| GPT-5.3-Codex | 400K | 1x | Ваш основной инструмент для WSL/Terraform/Go. | +| Claude Opus 4.6 | 192K | 3x | Элитный кодер. Самый чистый и логичный код. | +| Claude Sonnet 4.6 | 160K | 1x | Баланс для повседневных фич. | +| GPT-5.4 mini | 400K | 0.33x | Лучший для тестов и мелких правок «пачками». | +| Raptor mini | 264K | 0x | Бесплатный анализ больших логов и дампов. | +| Grok Code Fast | 173K | 0.25x | Ультра‑свежие данные и библиотеки. | +| GPT-4o / GPT-4.1 | ~100K | 0x | Для элементарных задач и Bash‑скриптов. | + +> Примечание: сохранённая версия также будет скопирована в домашнюю папку ВМ как `~/agent_models.md`. diff --git a/doc/models/available_models_list.md b/doc/models/available_models_list.md new file mode 100644 index 0000000..6e1664b --- /dev/null +++ b/doc/models/available_models_list.md @@ -0,0 +1,28 @@ +# Список доступных языковых моделей (скриншот) + +Дата: 2026-04-13 + +Колонки: Имя | Размер контекста | Возможности | Множитель запроса + +- Claude Haiku 4.5 — 160K — Инструменты, Видение — 0.33x +- Claude Opus 4.5 — 160K — Инструменты, Видение — 3x +- Claude Opus 4.6 — 192K — Инструменты, Видение — 3x +- Claude Sonnet 4 — 144K — Инструменты, Видение — 1x +- Claude Sonnet 4.5 — 160K — Инструменты, Видение — 1x +- Claude Sonnet 4.6 — 160K — Инструменты, Видение — 1x +- Gemini 2.5 Pro — 173K — Инструменты, Видение — 1x +- Gemini 3 Flash (Preview) — 173K — Инструменты, Видение — 0.33x +- Gemini 3.1 Pro (Preview) — 173K — Инструменты, Видение — 1x +- GPT-4.1 — 128K — Инструменты, Видение — 0x +- GPT-4o — 68K — Инструменты, Видение — 0x +- GPT-5 mini — 192K — Инструменты, Видение — 1x +- GPT-5.1 — 192K — Инструменты, Видение — 1x +- GPT-5.2 — 192K — Инструменты, Видение — 1x +- GPT-5.2-Codex — 400K — Инструменты, Видение — 1x +- GPT-5.3-Codex — 400K — Инструменты, Видение — 1x +- GPT-5.4 — 400K — Инструменты, Видение — 1x +- GPT-5.4 mini — 400K — Инструменты, Видение — 0.33x +- Grok Code Fast 1 — 173K — Инструменты, Видение — 0.25x +- Raptor mini (Preview) — 264K — Инструменты, Видение — 0x + +Примечание: транскрипция выполнена по приложенному скриншоту. Если нужно другое форматирование (CSV, JSON или добавить дополнительные колонки), скажите, сохраню в нужном виде. diff --git a/sqs-operator/quick_test.sh b/sqs-operator/quick_test.sh new file mode 100644 index 0000000..d128935 --- /dev/null +++ b/sqs-operator/quick_test.sh @@ -0,0 +1,64 @@ +#!/bin/bash +# quick_test.sh — Быстрая проверка SQS-сервиса +# Проверяет: создание очереди, отправку, получение, удаление +# Требования: curl +# Запуск: bash quick_test.sh + +set -euo pipefail + +SQS_ENDPOINT="https://sqs.kube5s.ru/sqs/test001" +ACCESS_KEY="SQSAK-test001-ae4dc8" +SECRET_KEY="J8mElXq8Nwww8iFRtGJCm8KfxM9XSTluTqS2A67xsEE" +AUTH="${ACCESS_KEY}:${SECRET_KEY}" +VERSION="2012-11-05" + +PASS=0 +FAIL=0 +QUEUE="quicktest-$$" + +call() { + curl -sf --max-time 15 -u "$AUTH" "${SQS_ENDPOINT}?${1}&Version=${VERSION}" 2>/dev/null +} + +check() { + if echo "$2" | grep -q "$3"; then + PASS=$((PASS + 1)); echo " ✅ $1" + else + FAIL=$((FAIL + 1)); echo " ❌ $1" + fi +} + +echo "=== SQS Quick Test ===" +echo "" + +# 1. Создать очередь +R=$(call "Action=CreateQueue&QueueName=${QUEUE}") +check "Создание очереди" "$R" "CreateQueueResult" + +# 2. Список очередей +R=$(call "Action=ListQueues") +check "Очередь в списке" "$R" "${QUEUE}" + +# 3. Отправить сообщение +R=$(call "Action=SendMessage&QueueUrl=${SQS_ENDPOINT}/test001/${QUEUE}&MessageBody=HelloSQS") +check "Отправка сообщения" "$R" "MessageId" + +# 4. Получить сообщение +R=$(call "Action=ReceiveMessage&QueueUrl=${SQS_ENDPOINT}/test001/${QUEUE}") +check "Получение сообщения" "$R" "HelloSQS" + +# 5. Удалить очередь +R=$(call "Action=DeleteQueue&QueueUrl=${SQS_ENDPOINT}/test001/${QUEUE}") +check "Удаление очереди" "$R" "DeleteQueueResponse" + +# 6. Очередь исчезла +R=$(call "Action=ListQueues") +if echo "$R" | grep -q "${QUEUE}"; then + FAIL=$((FAIL + 1)); echo " ❌ Очередь удалена из списка" +else + PASS=$((PASS + 1)); echo " ✅ Очередь удалена из списка" +fi + +echo "" +echo "Результат: ${PASS} OK / ${FAIL} FAIL" +[ "$FAIL" -eq 0 ] && echo "Всё работает!" || exit 1 diff --git a/sqs-operator/sqs_api_tests.sh b/sqs-operator/sqs_api_tests.sh new file mode 100644 index 0000000..8e0878b --- /dev/null +++ b/sqs-operator/sqs_api_tests.sh @@ -0,0 +1,486 @@ +#!/bin/bash +# 2026-04-09 — Полный набор тестов SQS API для ручного / автоматического запуска +# Покрывает: happy path, невалидный ввод, edge cases, безопасность +# Запуск: bash sqs_api_tests.sh +# Требования: curl, base64, доступ к https://sqs.kube5s.ru + +set -euo pipefail + +# ═══════════════════════════════════════════ +# КОНФИГУРАЦИЯ +# ═══════════════════════════════════════════ +SQS_HOST="https://sqs.kube5s.ru" +SQS_BASE="${SQS_HOST}/sqs/test001" +AK="SQSAK-test001-ae4dc8" +SK="J8mElXq8Nwww8iFRtGJCm8KfxM9XSTluTqS2A67xsEE" +AUTH="${AK}:${SK}" +VERSION="2012-11-05" + +# Счётчики результатов +PASS=0 +FAIL=0 +SKIP=0 +TOTAL=0 + +# Уникальный суффикс чтобы тесты не конфликтовали при повторном запуске +TS=$(date +%s) + +# ═══════════════════════════════════════════ +# УТИЛИТЫ +# ═══════════════════════════════════════════ + +sqs_call() { + # $1 = параметры запроса (после ?) + # $2 = auth (опционально, по умолчанию $AUTH) + local auth="${2:-$AUTH}" + curl -s --max-time 15 -u "$auth" "${SQS_BASE}?${1}&Version=${VERSION}" 2>&1 +} + +assert_contains() { + # $1 = название теста + # $2 = ответ + # $3 = ожидаемая подстрока + TOTAL=$((TOTAL + 1)) + if echo "$2" | grep -q "$3"; then + PASS=$((PASS + 1)) + echo " ✅ PASS: $1" + else + FAIL=$((FAIL + 1)) + echo " ❌ FAIL: $1" + echo " Ожидалось: '$3'" + echo " Получено: $(echo "$2" | head -3)" + fi +} + +assert_not_contains() { + # $1 = название теста + # $2 = ответ + # $3 = подстрока которой НЕ должно быть + TOTAL=$((TOTAL + 1)) + if echo "$2" | grep -q "$3"; then + FAIL=$((FAIL + 1)) + echo " ❌ FAIL: $1" + echo " НЕ ожидалось: '$3'" + echo " Получено: $(echo "$2" | head -3)" + else + PASS=$((PASS + 1)) + echo " ✅ PASS: $1" + fi +} + +assert_http_code() { + # $1 = название теста + # $2 = URL (полный) + # $3 = ожидаемый HTTP код + # $4 = auth (опционально) + TOTAL=$((TOTAL + 1)) + local auth="${4:-$AUTH}" + local code + code=$(curl -s -o /dev/null -w "%{http_code}" --max-time 15 -u "$auth" "$2" 2>&1) + if [ "$code" = "$3" ]; then + PASS=$((PASS + 1)) + echo " ✅ PASS: $1 (HTTP $code)" + else + FAIL=$((FAIL + 1)) + echo " ❌ FAIL: $1 (ожидался HTTP $3, получен $code)" + fi +} + +cleanup_queue() { + # Тихое удаление очереди + sqs_call "Action=DeleteQueue&QueueUrl=${SQS_BASE}/test001/$1" >/dev/null 2>&1 || true +} + +# ═══════════════════════════════════════════ +echo "╔══════════════════════════════════════════════════╗" +echo "║ SQS API Test Suite — $(date '+%Y-%m-%d %H:%M:%S') ║" +echo "║ Endpoint: ${SQS_BASE} ║" +echo "╚══════════════════════════════════════════════════╝" +echo "" + +# ═══════════════════════════════════════════ +# ГРУППА 1: БАЗОВЫЕ ОПЕРАЦИИ (Happy Path) +# ═══════════════════════════════════════════ +echo "── ГРУППА 1: Базовые операции (Happy Path) ──" + +Q1="testq-hp-${TS}" + +# T01: Создание очереди +R=$(sqs_call "Action=CreateQueue&QueueName=${Q1}") +assert_contains "T01 CreateQueue" "$R" "CreateQueueResult" +assert_contains "T01 CreateQueue URL" "$R" "${Q1}" + +# T02: Повторное создание той же очереди — не ошибка, возвращает ту же URL +R=$(sqs_call "Action=CreateQueue&QueueName=${Q1}") +assert_contains "T02 CreateQueue idempotent" "$R" "CreateQueueResult" + +# T03: ListQueues — очередь видна +R=$(sqs_call "Action=ListQueues") +assert_contains "T03 ListQueues" "$R" "${Q1}" + +# T04: SendMessage +R=$(sqs_call "Action=SendMessage&QueueUrl=${SQS_BASE}/test001/${Q1}&MessageBody=Hello") +assert_contains "T04 SendMessage" "$R" "MessageId" +assert_contains "T04 SendMessage MD5" "$R" "MD5OfMessageBody" + +# T05: ReceiveMessage +R=$(sqs_call "Action=ReceiveMessage&QueueUrl=${SQS_BASE}/test001/${Q1}") +assert_contains "T05 ReceiveMessage Body" "$R" "Hello" +assert_contains "T05 ReceiveMessage ReceiptHandle" "$R" "ReceiptHandle" + +# Извлекаем ReceiptHandle для T06 +RH=$(echo "$R" | grep -oP '(?<=)[^<]+' | head -1) + +# T06: DeleteMessage +# ReceiptHandle содержит +/= — URL-кодируем чтобы не сломать query string +if [ -n "$RH" ]; then + RH_ENC=$(python3 -c "import urllib.parse,sys; print(urllib.parse.quote(sys.argv[1], safe=''))" "$RH") + R=$(sqs_call "Action=DeleteMessage&QueueUrl=${SQS_BASE}/test001/${Q1}&ReceiptHandle=${RH_ENC}") + assert_contains "T06 DeleteMessage" "$R" "DeleteMessageResponse" +else + TOTAL=$((TOTAL + 1)); SKIP=$((SKIP + 1)) + echo " ⚠️ SKIP: T06 DeleteMessage — не удалось получить ReceiptHandle" +fi + +# T07: ReceiveMessage из пустой очереди — пустой результат +R=$(sqs_call "Action=ReceiveMessage&QueueUrl=${SQS_BASE}/test001/${Q1}") +assert_not_contains "T07 ReceiveMessage empty queue" "$R" "" + +# T08: GetQueueAttributes +R=$(sqs_call "Action=GetQueueAttributes&QueueUrl=${SQS_BASE}/test001/${Q1}&AttributeName.1=All") +assert_contains "T08 GetQueueAttributes" "$R" "GetQueueAttributesResponse" + +# T09: PurgeQueue +sqs_call "Action=SendMessage&QueueUrl=${SQS_BASE}/test001/${Q1}&MessageBody=ToPurge" >/dev/null +R=$(sqs_call "Action=PurgeQueue&QueueUrl=${SQS_BASE}/test001/${Q1}") +assert_contains "T09 PurgeQueue" "$R" "PurgeQueueResponse" + +# T10: DeleteQueue +R=$(sqs_call "Action=DeleteQueue&QueueUrl=${SQS_BASE}/test001/${Q1}") +assert_contains "T10 DeleteQueue" "$R" "DeleteQueueResponse" + +# T11: ListQueues — очередь исчезла +R=$(sqs_call "Action=ListQueues") +assert_not_contains "T11 ListQueues after delete" "$R" "${Q1}" + +echo "" + +# ═══════════════════════════════════════════ +# ГРУППА 2: НЕВАЛИДНЫЙ ВВОД +# ═══════════════════════════════════════════ +echo "── ГРУППА 2: Невалидный ввод ──" + +# T12: Неизвестный Action +R=$(sqs_call "Action=BogusAction") +assert_contains "T12 Unknown Action" "$R" "InvalidAction\|AWS.SimpleQueueService.NonExistentAction\|UnknownAction\|InvalidParameterValue" + +# T13: Пустой Action +R=$(sqs_call "Action=") +assert_contains "T13 Empty Action" "$R" "Error\|MissingAction\|InvalidAction\|Value for parameter Action is not valid" + +# T14: Без Action вообще +R=$(curl -s --max-time 15 -u "$AUTH" "${SQS_BASE}?Version=${VERSION}" 2>&1) +assert_contains "T14 Missing Action" "$R" "Error\|MissingAction\|request" + +# T15: CreateQueue без QueueName +R=$(sqs_call "Action=CreateQueue") +assert_contains "T15 CreateQueue no name" "$R" "Error\|MissingParameter\|Value for parameter QueueName is not valid" + +# T16: CreateQueue с пустым QueueName +R=$(sqs_call "Action=CreateQueue&QueueName=") +assert_contains "T16 CreateQueue empty name" "$R" "Error\|InvalidParameterValue" + +# T17: CreateQueue с недопустимыми символами (пробелы, кириллица) +# Akka HTTP (ElasticMQ) отвечает "Illegal request-target" на raw кириллицу +R=$(sqs_call "Action=CreateQueue&QueueName=очередь%20тест") +assert_contains "T17 CreateQueue invalid chars" "$R" "Error\|InvalidParameterValue\|Illegal" + +# T18: CreateQueue с очень длинным именем (>80 символов) +LONGNAME=$(printf 'q%.0s' {1..100}) +R=$(sqs_call "Action=CreateQueue&QueueName=${LONGNAME}") +assert_contains "T18 CreateQueue name too long" "$R" "Error\|InvalidParameterValue\|CreateQueueResult" +# Примечание: ElasticMQ может создать — это допустимо если не strict AWS + +# T19: SendMessage без MessageBody +R=$(sqs_call "Action=SendMessage&QueueUrl=${SQS_BASE}/test001/nonexist") +assert_contains "T19 SendMessage no body" "$R" "Error\|MissingParameter\|The request must contain the parameter MessageBody" + +# T20: SendMessage без QueueUrl +R=$(sqs_call "Action=SendMessage&MessageBody=test") +assert_contains "T20 SendMessage no QueueUrl" "$R" "Error\|MissingParameter\|AWS.SimpleQueueService.NonExistentQueue\|The request must contain" + +echo "" + +# ═══════════════════════════════════════════ +# ГРУППА 3: НЕСУЩЕСТВУЮЩИЕ РЕСУРСЫ +# ═══════════════════════════════════════════ +echo "── ГРУППА 3: Несуществующие ресурсы ──" + +# T21: SendMessage в несуществующую очередь (autoCreateQueues=false!) +R=$(sqs_call "Action=SendMessage&QueueUrl=${SQS_BASE}/test001/nonexist-${TS}&MessageBody=fail") +assert_contains "T21 SendMessage nonexistent queue" "$R" "NonExistentQueue\|AWS.SimpleQueueService.NonExistentQueue\|does not exist\|Error" + +# T22: ReceiveMessage из несуществующей очереди +R=$(sqs_call "Action=ReceiveMessage&QueueUrl=${SQS_BASE}/test001/nonexist-${TS}") +assert_contains "T22 ReceiveMessage nonexistent queue" "$R" "NonExistentQueue\|AWS.SimpleQueueService.NonExistentQueue\|does not exist\|Error" + +# T23: DeleteQueue несуществующей очереди +R=$(sqs_call "Action=DeleteQueue&QueueUrl=${SQS_BASE}/test001/nonexist-${TS}") +assert_contains "T23 DeleteQueue nonexistent" "$R" "NonExistentQueue\|AWS.SimpleQueueService.NonExistentQueue\|does not exist\|Error" + +# T24: GetQueueAttributes несуществующей очереди +R=$(sqs_call "Action=GetQueueAttributes&QueueUrl=${SQS_BASE}/test001/nonexist-${TS}&AttributeName.1=All") +assert_contains "T24 GetQueueAttributes nonexistent" "$R" "NonExistentQueue\|AWS.SimpleQueueService.NonExistentQueue\|does not exist\|Error" + +# T25: DeleteMessage с фейковым ReceiptHandle +Q25="testq-rh-${TS}" +sqs_call "Action=CreateQueue&QueueName=${Q25}" >/dev/null +R=$(sqs_call "Action=DeleteMessage&QueueUrl=${SQS_BASE}/test001/${Q25}&ReceiptHandle=fake-receipt-handle-12345") +assert_contains "T25 DeleteMessage fake receipt" "$R" "Error\|ReceiptHandleIsInvalid\|DeleteMessageResponse" +# Примечание: ElasticMQ может молча игнорировать — это допустимо +cleanup_queue "$Q25" + +echo "" + +# ═══════════════════════════════════════════ +# ГРУППА 4: АУТЕНТИФИКАЦИЯ И БЕЗОПАСНОСТЬ +# ═══════════════════════════════════════════ +echo "── ГРУППА 4: Аутентификация и безопасность ──" + +# T26: Неправильный Access Key +R=$(sqs_call "Action=ListQueues" "WRONG-KEY:${SK}") +assert_contains "T26 Wrong access key" "$R" "InvalidClientTokenId\|SignatureDoesNotMatch\|Error\|403\|401\|Unauthorized" + +# T27: Неправильный Secret Key +R=$(sqs_call "Action=ListQueues" "${AK}:WRONG-SECRET") +assert_contains "T27 Wrong secret key" "$R" "SignatureDoesNotMatch\|Error\|403\|401\|Unauthorized" + +# T28: Пустые credentials +R=$(curl -s --max-time 15 "${SQS_BASE}?Action=ListQueues&Version=${VERSION}" 2>&1) +assert_contains "T28 No credentials" "$R" "Error\|AuthFailure\|MissingAuthenticationToken\|ListQueuesResult\|403\|401" +# Примечание: ElasticMQ может не требовать auth — если так, это проблема безопасности + +# T29: SQL injection в QueueName +R=$(sqs_call "Action=CreateQueue&QueueName=test';DROP%20TABLE%20queues;--") +assert_contains "T29 SQL injection in QueueName" "$R" "Error\|InvalidParameterValue\|CreateQueueResult" +# Если создалась — OK, ElasticMQ in-memory, нет SQL. Убираем. +cleanup_queue "test';DROP TABLE queues;--" + +# T30: XSS в MessageBody +Q30="testq-xss-${TS}" +sqs_call "Action=CreateQueue&QueueName=${Q30}" >/dev/null +R=$(sqs_call "Action=SendMessage&QueueUrl=${SQS_BASE}/test001/${Q30}&MessageBody=%3Cscript%3Ealert(1)%3C/script%3E") +assert_contains "T30 XSS in MessageBody — accepted" "$R" "MessageId" +# SQS должен принять любой body — это не HTML, а очередь. Проверяем что возвращает как есть: +R=$(sqs_call "Action=ReceiveMessage&QueueUrl=${SQS_BASE}/test001/${Q30}") +assert_contains "T30 XSS body returned" "$R" "script\|<script" +cleanup_queue "$Q30" + +echo "" + +# ═══════════════════════════════════════════ +# ГРУППА 5: СПЕЦСИМВОЛЫ И КОДИРОВКИ +# ═══════════════════════════════════════════ +echo "── ГРУППА 5: Спецсимволы и кодировки ──" + +Q5="testq-enc-${TS}" +sqs_call "Action=CreateQueue&QueueName=${Q5}" >/dev/null + +# T31: Unicode в MessageBody +R=$(sqs_call "Action=SendMessage&QueueUrl=${SQS_BASE}/test001/${Q5}&MessageBody=%D0%9F%D1%80%D0%B8%D0%B2%D0%B5%D1%82") +assert_contains "T31 Unicode body" "$R" "MessageId" + +# T32: Receive Unicode +R=$(sqs_call "Action=ReceiveMessage&QueueUrl=${SQS_BASE}/test001/${Q5}") +assert_contains "T32 Receive Unicode" "$R" "Привет\|%D0%9F" + +# T33: Амперсанд и XML-спецсимволы в body +R=$(sqs_call "Action=SendMessage&QueueUrl=${SQS_BASE}/test001/${Q5}&MessageBody=a%26b%3Cc%3Ed") +assert_contains "T33 XML special chars" "$R" "MessageId" + +# T34: JSON в body +R=$(sqs_call "Action=SendMessage&QueueUrl=${SQS_BASE}/test001/${Q5}&MessageBody=%7B%22key%22%3A%22value%22%7D") +assert_contains "T34 JSON body" "$R" "MessageId" + +# T35: Пустой MessageBody +R=$(sqs_call "Action=SendMessage&QueueUrl=${SQS_BASE}/test001/${Q5}&MessageBody=") +assert_contains "T35 Empty body" "$R" "Error\|MissingParameter\|MessageId" + +cleanup_queue "$Q5" + +echo "" + +# ═══════════════════════════════════════════ +# ГРУППА 6: НАГРУЗКА И ПОРЯДОК +# ═══════════════════════════════════════════ +echo "── ГРУППА 6: Множественные сообщения ──" + +Q6="testq-multi-${TS}" +sqs_call "Action=CreateQueue&QueueName=${Q6}" >/dev/null + +# T36: Отправить 10 сообщений подряд +ALL_OK=true +for i in $(seq 1 10); do + R=$(sqs_call "Action=SendMessage&QueueUrl=${SQS_BASE}/test001/${Q6}&MessageBody=msg-${i}") + if ! echo "$R" | grep -q "MessageId"; then + ALL_OK=false + break + fi +done +TOTAL=$((TOTAL + 1)) +if $ALL_OK; then + PASS=$((PASS + 1)) + echo " ✅ PASS: T36 Send 10 messages" +else + FAIL=$((FAIL + 1)) + echo " ❌ FAIL: T36 Send 10 messages" +fi + +# T37: Receive все 10 (может потребовать несколько вызовов) +RECEIVED=0 +for _ in $(seq 1 15); do + R=$(sqs_call "Action=ReceiveMessage&QueueUrl=${SQS_BASE}/test001/${Q6}&MaxNumberOfMessages=10") + COUNT=$(echo "$R" | grep -c "" || true) + RECEIVED=$((RECEIVED + COUNT)) + if [ "$RECEIVED" -ge 10 ]; then break; fi +done +TOTAL=$((TOTAL + 1)) +if [ "$RECEIVED" -ge 10 ]; then + PASS=$((PASS + 1)) + echo " ✅ PASS: T37 Receive all 10 messages (got ${RECEIVED})" +else + FAIL=$((FAIL + 1)) + echo " ❌ FAIL: T37 Receive all 10 messages (got ${RECEIVED}/10)" +fi + +cleanup_queue "$Q6" + +echo "" + +# ═══════════════════════════════════════════ +# ГРУППА 7: HTTP-УРОВЕНЬ +# ═══════════════════════════════════════════ +echo "── ГРУППА 7: HTTP-уровень ──" + +# T38: Проверка HTTPS +assert_http_code "T38 HTTPS endpoint" "${SQS_BASE}?Action=ListQueues&Version=${VERSION}" "200" + +# T39: Health endpoint +TOTAL=$((TOTAL + 1)) +HC=$(curl -s -o /dev/null -w "%{http_code}" --max-time 10 "${SQS_BASE%/sqs/test001}/sqs/test001/health" 2>&1 || echo "000") +# Примечание: health может быть на другом пути +if [ "$HC" = "200" ] || [ "$HC" = "404" ]; then + PASS=$((PASS + 1)) + echo " ✅ PASS: T39 Health endpoint (HTTP $HC)" +else + SKIP=$((SKIP + 1)) + echo " ⚠️ SKIP: T39 Health endpoint (HTTP $HC — может быть не на этом пути)" +fi + +# T40: Очень большой запрос (64KB body) +Q40="testq-big-${TS}" +sqs_call "Action=CreateQueue&QueueName=${Q40}" >/dev/null +BIGBODY=$(python3 -c "print('A'*65000)" 2>/dev/null || printf 'A%.0s' $(seq 1 65000)) +R=$(curl -s --max-time 15 -u "$AUTH" -X POST "${SQS_BASE}?Action=SendMessage&QueueUrl=${SQS_BASE}/test001/${Q40}&Version=${VERSION}" --data-urlencode "MessageBody=${BIGBODY}" 2>&1) +assert_contains "T40 Large body 64KB" "$R" "MessageId\|Error\|TooLong\|InvalidParameterValue" +cleanup_queue "$Q40" + +# T41: POST вместо GET +R=$(curl -s --max-time 15 -u "$AUTH" -X POST "${SQS_BASE}?Action=ListQueues&Version=${VERSION}" 2>&1) +assert_contains "T41 POST method" "$R" "ListQueuesResult\|ListQueuesResponse" + +# T42: Несуществующий путь +TOTAL=$((TOTAL + 1)) +HC=$(curl -s -o /dev/null -w "%{http_code}" --max-time 10 -u "$AUTH" "${SQS_HOST}/sqs/nonexistent-tenant?Action=ListQueues&Version=${VERSION}" 2>&1) +if [ "$HC" = "404" ] || [ "$HC" = "403" ] || [ "$HC" = "502" ] || [ "$HC" = "503" ] || [ "$HC" = "302" ]; then + PASS=$((PASS + 1)) + echo " ✅ PASS: T42 Wrong tenant path (HTTP $HC)" +else + FAIL=$((FAIL + 1)) + echo " ❌ FAIL: T42 Wrong tenant path (HTTP $HC — ожидался 302/404/403)" +fi + +echo "" + +# ═══════════════════════════════════════════ +# ГРУППА 8: ТАЙМАУТЫ И LONG POLLING +# ═══════════════════════════════════════════ +echo "── ГРУППА 8: Long polling и таймауты ──" + +Q8="testq-poll-${TS}" +sqs_call "Action=CreateQueue&QueueName=${Q8}" >/dev/null + +# T43: WaitTimeSeconds=1 на пустой очереди — должен вернуть пустой через ~1 сек +TOTAL=$((TOTAL + 1)) +T_START=$(date +%s) +R=$(sqs_call "Action=ReceiveMessage&QueueUrl=${SQS_BASE}/test001/${Q8}&WaitTimeSeconds=1") +T_END=$(date +%s) +ELAPSED=$((T_END - T_START)) +if [ "$ELAPSED" -ge 1 ] && [ "$ELAPSED" -le 5 ]; then + PASS=$((PASS + 1)) + echo " ✅ PASS: T43 Long poll 1s empty queue (${ELAPSED}s)" +else + FAIL=$((FAIL + 1)) + echo " ❌ FAIL: T43 Long poll 1s (${ELAPSED}s — ожидалось 1-5s)" +fi + +# T44: WaitTimeSeconds=0 — мгновенный ответ +TOTAL=$((TOTAL + 1)) +T_START=$(date +%s) +R=$(sqs_call "Action=ReceiveMessage&QueueUrl=${SQS_BASE}/test001/${Q8}&WaitTimeSeconds=0") +T_END=$(date +%s) +ELAPSED=$((T_END - T_START)) +if [ "$ELAPSED" -le 2 ]; then + PASS=$((PASS + 1)) + echo " ✅ PASS: T44 Short poll instant response (${ELAPSED}s)" +else + FAIL=$((FAIL + 1)) + echo " ❌ FAIL: T44 Short poll (${ELAPSED}s — ожидалось <2s)" +fi + +cleanup_queue "$Q8" + +echo "" + +# ═══════════════════════════════════════════ +# ГРУППА 9: VISIBILITY TIMEOUT +# ═══════════════════════════════════════════ +echo "── ГРУППА 9: Visibility timeout ──" + +Q9="testq-vis-${TS}" +sqs_call "Action=CreateQueue&QueueName=${Q9}" >/dev/null +sqs_call "Action=SendMessage&QueueUrl=${SQS_BASE}/test001/${Q9}&MessageBody=VisTest" >/dev/null + +# T45: Receive с VisibilityTimeout=2 — сообщение скрыто, повторный receive пуст +R=$(sqs_call "Action=ReceiveMessage&QueueUrl=${SQS_BASE}/test001/${Q9}&VisibilityTimeout=2") +assert_contains "T45a Receive with visibility" "$R" "VisTest" + +R=$(sqs_call "Action=ReceiveMessage&QueueUrl=${SQS_BASE}/test001/${Q9}&WaitTimeSeconds=0") +assert_not_contains "T45b Hidden during visibility" "$R" "VisTest" + +# T46: После visibility timeout — сообщение снова доступно +sleep 3 +R=$(sqs_call "Action=ReceiveMessage&QueueUrl=${SQS_BASE}/test001/${Q9}&WaitTimeSeconds=0") +assert_contains "T46 Re-visible after timeout" "$R" "VisTest" + +cleanup_queue "$Q9" + +echo "" + +# ═══════════════════════════════════════════ +# РЕЗУЛЬТАТЫ +# ═══════════════════════════════════════════ +echo "╔══════════════════════════════════════════════════╗" +echo "║ РЕЗУЛЬТАТЫ ║" +echo "╠══════════════════════════════════════════════════╣" +echo "║ Всего: ${TOTAL} ║" +echo "║ ✅ PASS: ${PASS} ║" +echo "║ ❌ FAIL: ${FAIL} ║" +echo "║ ⚠️ SKIP: ${SKIP} ║" +echo "╚══════════════════════════════════════════════════╝" + +if [ "$FAIL" -gt 0 ]; then + exit 1 +else + exit 0 +fi diff --git a/sqs-operator/sqs_load_test.sh b/sqs-operator/sqs_load_test.sh new file mode 100644 index 0000000..4d2f42e --- /dev/null +++ b/sqs-operator/sqs_load_test.sh @@ -0,0 +1,426 @@ +#!/bin/bash +# 2026-04-09 — Нагрузочное тестирование SQS API +# Замеряет: throughput, latency, конкурентность, стабильность под нагрузкой +# Запуск: bash sqs_load_test.sh + +set -euo pipefail + +# ═══════════════════════════════════════════ +# КОНФИГУРАЦИЯ +# ═══════════════════════════════════════════ +export PATH="$HOME/.local/bin:$PATH" +SQS_HOST="https://sqs.kube5s.ru" +SQS_BASE="${SQS_HOST}/sqs/test001" +export AWS_ACCESS_KEY_ID="SQSAK-test001-ae4dc8" +export AWS_SECRET_ACCESS_KEY="J8mElXq8Nwww8iFRtGJCm8KfxM9XSTluTqS2A67xsEE" +export AWS_DEFAULT_REGION="us-east-1" +ENDPOINT="--endpoint-url ${SQS_BASE}" +AUTH="${AWS_ACCESS_KEY_ID}:${AWS_SECRET_ACCESS_KEY}" +VERSION="2012-11-05" +TS=$(date +%s) +TMPDIR=$(mktemp -d /tmp/sqs-load-XXXXX) + +# Параметры нагрузки +MESSAGES_SINGLE=100 # send/receive в одну очередь +MESSAGES_BURST=50 # burst за раз +PARALLEL_WORKERS=5 # параллельные воркеры +QUEUES_COUNT=10 # кол-во очередей для multi-queue теста + +echo "╔══════════════════════════════════════════════════════╗" +echo "║ SQS Load Test — $(date '+%Y-%m-%d %H:%M:%S') ║" +echo "║ Endpoint: ${SQS_BASE} ║" +echo "║ Temp: ${TMPDIR} ║" +echo "╚══════════════════════════════════════════════════════╝" +echo "" + +# Утилита замера времени (ms) +ms_now() { date +%s%N | cut -b1-13; } + +sqs_call() { + curl -s --max-time 30 -u "$AUTH" "${SQS_BASE}?${1}&Version=${VERSION}" 2>&1 +} + +# ═══════════════════════════════════════════ +# ТЕСТ 1: Latency отдельных операций +# ═══════════════════════════════════════════ +echo "── ТЕСТ 1: Latency отдельных операций ──" + +Q1="loadq-latency-${TS}" +sqs_call "Action=CreateQueue&QueueName=${Q1}" >/dev/null + +# SendMessage latency (20 вызовов) +SEND_TIMES=() +for i in $(seq 1 20); do + T1=$(ms_now) + sqs_call "Action=SendMessage&QueueUrl=${SQS_BASE}/test001/${Q1}&MessageBody=latency-test-${i}" >/dev/null + T2=$(ms_now) + SEND_TIMES+=( $((T2 - T1)) ) +done + +# Статистика +SEND_TOTAL=0 +SEND_MIN=999999 +SEND_MAX=0 +for t in "${SEND_TIMES[@]}"; do + SEND_TOTAL=$((SEND_TOTAL + t)) + [ "$t" -lt "$SEND_MIN" ] && SEND_MIN=$t + [ "$t" -gt "$SEND_MAX" ] && SEND_MAX=$t +done +SEND_AVG=$((SEND_TOTAL / ${#SEND_TIMES[@]})) +echo " SendMessage (20x): avg=${SEND_AVG}ms min=${SEND_MIN}ms max=${SEND_MAX}ms" + +# ReceiveMessage latency (20 вызовов) +RECV_TIMES=() +for i in $(seq 1 20); do + T1=$(ms_now) + sqs_call "Action=ReceiveMessage&QueueUrl=${SQS_BASE}/test001/${Q1}&MaxNumberOfMessages=1" >/dev/null + T2=$(ms_now) + RECV_TIMES+=( $((T2 - T1)) ) +done + +RECV_TOTAL=0 +RECV_MIN=999999 +RECV_MAX=0 +for t in "${RECV_TIMES[@]}"; do + RECV_TOTAL=$((RECV_TOTAL + t)) + [ "$t" -lt "$RECV_MIN" ] && RECV_MIN=$t + [ "$t" -gt "$RECV_MAX" ] && RECV_MAX=$t +done +RECV_AVG=$((RECV_TOTAL / ${#RECV_TIMES[@]})) +echo " ReceiveMessage (20x): avg=${RECV_AVG}ms min=${RECV_MIN}ms max=${RECV_MAX}ms" + +# ListQueues latency +LIST_TIMES=() +for i in $(seq 1 10); do + T1=$(ms_now) + sqs_call "Action=ListQueues" >/dev/null + T2=$(ms_now) + LIST_TIMES+=( $((T2 - T1)) ) +done +LIST_TOTAL=0 +for t in "${LIST_TIMES[@]}"; do LIST_TOTAL=$((LIST_TOTAL + t)); done +LIST_AVG=$((LIST_TOTAL / ${#LIST_TIMES[@]})) +echo " ListQueues (10x): avg=${LIST_AVG}ms" + +sqs_call "Action=PurgeQueue&QueueUrl=${SQS_BASE}/test001/${Q1}" >/dev/null +sqs_call "Action=DeleteQueue&QueueUrl=${SQS_BASE}/test001/${Q1}" >/dev/null +echo "" + +# ═══════════════════════════════════════════ +# ТЕСТ 2: Последовательный throughput — 100 сообщений +# ═══════════════════════════════════════════ +echo "── ТЕСТ 2: Последовательный throughput (${MESSAGES_SINGLE} сообщений) ──" + +Q2="loadq-seq-${TS}" +sqs_call "Action=CreateQueue&QueueName=${Q2}" >/dev/null + +# Отправка +T_START=$(ms_now) +SEND_OK=0 +SEND_ERR=0 +for i in $(seq 1 $MESSAGES_SINGLE); do + R=$(sqs_call "Action=SendMessage&QueueUrl=${SQS_BASE}/test001/${Q2}&MessageBody=seq-msg-${i}") + if echo "$R" | grep -q "MessageId"; then + SEND_OK=$((SEND_OK + 1)) + else + SEND_ERR=$((SEND_ERR + 1)) + fi +done +T_END=$(ms_now) +SEND_ELAPSED=$((T_END - T_START)) +SEND_RPS=$(( MESSAGES_SINGLE * 1000 / (SEND_ELAPSED + 1) )) +echo " Send: ${SEND_OK} OK / ${SEND_ERR} ERR за ${SEND_ELAPSED}ms (${SEND_RPS} msg/s)" + +# Получение +T_START=$(ms_now) +RECV_OK=0 +EMPTY=0 +for attempt in $(seq 1 $((MESSAGES_SINGLE + 50))); do + R=$(sqs_call "Action=ReceiveMessage&QueueUrl=${SQS_BASE}/test001/${Q2}&MaxNumberOfMessages=10") + COUNT=$(echo "$R" | grep -c "" || true) + if [ "$COUNT" -gt 0 ]; then + RECV_OK=$((RECV_OK + COUNT)) + else + EMPTY=$((EMPTY + 1)) + [ "$EMPTY" -ge 5 ] && break + fi + [ "$RECV_OK" -ge "$MESSAGES_SINGLE" ] && break +done +T_END=$(ms_now) +RECV_ELAPSED=$((T_END - T_START)) +RECV_RPS=$(( RECV_OK * 1000 / (RECV_ELAPSED + 1) )) +echo " Recv: ${RECV_OK}/${MESSAGES_SINGLE} за ${RECV_ELAPSED}ms (${RECV_RPS} msg/s)" + +sqs_call "Action=DeleteQueue&QueueUrl=${SQS_BASE}/test001/${Q2}" >/dev/null +echo "" + +# ═══════════════════════════════════════════ +# ТЕСТ 3: Параллельная отправка (N воркеров) +# ═══════════════════════════════════════════ +echo "── ТЕСТ 3: Параллельная отправка (${PARALLEL_WORKERS} воркеров × ${MESSAGES_BURST} msg) ──" + +Q3="loadq-par-${TS}" +sqs_call "Action=CreateQueue&QueueName=${Q3}" >/dev/null + +# Функция воркера — записывает результат в файл +worker_send() { + local wid=$1 + local ok=0 + local err=0 + for i in $(seq 1 $MESSAGES_BURST); do + R=$(curl -s --max-time 30 -u "$AUTH" "${SQS_BASE}?Action=SendMessage&QueueUrl=${SQS_BASE}/test001/${Q3}&MessageBody=w${wid}-msg${i}&Version=${VERSION}" 2>&1) + if echo "$R" | grep -q "MessageId"; then + ok=$((ok + 1)) + else + err=$((err + 1)) + fi + done + echo "${ok}:${err}" > "${TMPDIR}/worker-${wid}.result" +} + +T_START=$(ms_now) +for w in $(seq 1 $PARALLEL_WORKERS); do + worker_send $w & +done +wait +T_END=$(ms_now) +PAR_ELAPSED=$((T_END - T_START)) + +# Собираем результаты +PAR_OK=0 +PAR_ERR=0 +for w in $(seq 1 $PARALLEL_WORKERS); do + if [ -f "${TMPDIR}/worker-${w}.result" ]; then + RES=$(cat "${TMPDIR}/worker-${w}.result") + PAR_OK=$((PAR_OK + ${RES%%:*})) + PAR_ERR=$((PAR_ERR + ${RES##*:})) + fi +done +PAR_TOTAL=$((PARALLEL_WORKERS * MESSAGES_BURST)) +PAR_RPS=$(( PAR_OK * 1000 / (PAR_ELAPSED + 1) )) +echo " Send: ${PAR_OK}/${PAR_TOTAL} OK, ${PAR_ERR} ERR за ${PAR_ELAPSED}ms (${PAR_RPS} msg/s)" + +# Параллельное получение +worker_recv() { + local wid=$1 + local ok=0 + local empty=0 + for _ in $(seq 1 $((MESSAGES_BURST + 20))); do + R=$(curl -s --max-time 30 -u "$AUTH" "${SQS_BASE}?Action=ReceiveMessage&QueueUrl=${SQS_BASE}/test001/${Q3}&MaxNumberOfMessages=10&Version=${VERSION}" 2>&1) + COUNT=$(echo "$R" | grep -c "" || true) + if [ "$COUNT" -gt 0 ]; then + ok=$((ok + COUNT)) + else + empty=$((empty + 1)) + [ "$empty" -ge 3 ] && break + fi + done + echo "${ok}" > "${TMPDIR}/recv-${wid}.result" +} + +T_START=$(ms_now) +for w in $(seq 1 $PARALLEL_WORKERS); do + worker_recv $w & +done +wait +T_END=$(ms_now) +RECV_PAR_ELAPSED=$((T_END - T_START)) + +RECV_PAR_OK=0 +for w in $(seq 1 $PARALLEL_WORKERS); do + if [ -f "${TMPDIR}/recv-${w}.result" ]; then + RECV_PAR_OK=$((RECV_PAR_OK + $(cat "${TMPDIR}/recv-${w}.result"))) + fi +done +RECV_PAR_RPS=$(( RECV_PAR_OK * 1000 / (RECV_PAR_ELAPSED + 1) )) +echo " Recv: ${RECV_PAR_OK}/${PAR_TOTAL} за ${RECV_PAR_ELAPSED}ms (${RECV_PAR_RPS} msg/s)" + +sqs_call "Action=DeleteQueue&QueueUrl=${SQS_BASE}/test001/${Q3}" >/dev/null +echo "" + +# ═══════════════════════════════════════════ +# ТЕСТ 4: Много очередей одновременно +# ═══════════════════════════════════════════ +echo "── ТЕСТ 4: Создание ${QUEUES_COUNT} очередей + send/receive в каждую ──" + +T_START=$(ms_now) +CREATE_OK=0 +for i in $(seq 1 $QUEUES_COUNT); do + R=$(sqs_call "Action=CreateQueue&QueueName=loadq-multi-${TS}-${i}") + if echo "$R" | grep -q "CreateQueueResult"; then + CREATE_OK=$((CREATE_OK + 1)) + fi +done +T_CRE=$(ms_now) +echo " Create: ${CREATE_OK}/${QUEUES_COUNT} за $((T_CRE - T_START))ms" + +# Send 5 msg в каждую параллельно +multi_send() { + local qi=$1 + local ok=0 + for m in $(seq 1 5); do + R=$(curl -s --max-time 30 -u "$AUTH" "${SQS_BASE}?Action=SendMessage&QueueUrl=${SQS_BASE}/test001/loadq-multi-${TS}-${qi}&MessageBody=multi-${qi}-${m}&Version=${VERSION}" 2>&1) + echo "$R" | grep -q "MessageId" && ok=$((ok + 1)) + done + echo "$ok" > "${TMPDIR}/multi-send-${qi}.result" +} + +T_START=$(ms_now) +for i in $(seq 1 $QUEUES_COUNT); do + multi_send $i & +done +wait +T_SEND=$(ms_now) + +MULTI_SEND_OK=0 +for i in $(seq 1 $QUEUES_COUNT); do + [ -f "${TMPDIR}/multi-send-${i}.result" ] && MULTI_SEND_OK=$((MULTI_SEND_OK + $(cat "${TMPDIR}/multi-send-${i}.result"))) +done +echo " Send (5 × ${QUEUES_COUNT}): ${MULTI_SEND_OK}/$((QUEUES_COUNT * 5)) за $((T_SEND - T_START))ms" + +# Receive из каждой +multi_recv() { + local qi=$1 + local ok=0 + for _ in $(seq 1 10); do + R=$(curl -s --max-time 30 -u "$AUTH" "${SQS_BASE}?Action=ReceiveMessage&QueueUrl=${SQS_BASE}/test001/loadq-multi-${TS}-${qi}&MaxNumberOfMessages=10&Version=${VERSION}" 2>&1) + COUNT=$(echo "$R" | grep -c "" || true) + ok=$((ok + COUNT)) + [ "$COUNT" -eq 0 ] && break + done + echo "$ok" > "${TMPDIR}/multi-recv-${qi}.result" +} + +T_START=$(ms_now) +for i in $(seq 1 $QUEUES_COUNT); do + multi_recv $i & +done +wait +T_RECV=$(ms_now) + +MULTI_RECV_OK=0 +for i in $(seq 1 $QUEUES_COUNT); do + [ -f "${TMPDIR}/multi-recv-${i}.result" ] && MULTI_RECV_OK=$((MULTI_RECV_OK + $(cat "${TMPDIR}/multi-recv-${i}.result"))) +done +echo " Recv: ${MULTI_RECV_OK}/$((QUEUES_COUNT * 5)) за $((T_RECV - T_START))ms" + +# Удаление +T_START=$(ms_now) +for i in $(seq 1 $QUEUES_COUNT); do + sqs_call "Action=DeleteQueue&QueueUrl=${SQS_BASE}/test001/loadq-multi-${TS}-${i}" >/dev/null & +done +wait +T_DEL=$(ms_now) +echo " Delete ${QUEUES_COUNT} queues: $((T_DEL - T_START))ms" +echo "" + +# ═══════════════════════════════════════════ +# ТЕСТ 5: Стресс — быстрая send+receive петля +# ═══════════════════════════════════════════ +echo "── ТЕСТ 5: Stress loop — send+receive 60 сек ──" + +Q5="loadq-stress-${TS}" +sqs_call "Action=CreateQueue&QueueName=${Q5}" >/dev/null + +STRESS_DURATION=60 +STRESS_SEND=0 +STRESS_RECV=0 +STRESS_ERR=0 +T_START=$(date +%s) + +while true; do + NOW=$(date +%s) + ELAPSED=$((NOW - T_START)) + [ "$ELAPSED" -ge "$STRESS_DURATION" ] && break + + # Send + R=$(sqs_call "Action=SendMessage&QueueUrl=${SQS_BASE}/test001/${Q5}&MessageBody=stress-${STRESS_SEND}") + if echo "$R" | grep -q "MessageId"; then + STRESS_SEND=$((STRESS_SEND + 1)) + else + STRESS_ERR=$((STRESS_ERR + 1)) + fi + + # Receive + R=$(sqs_call "Action=ReceiveMessage&QueueUrl=${SQS_BASE}/test001/${Q5}&MaxNumberOfMessages=10") + COUNT=$(echo "$R" | grep -c "" || true) + STRESS_RECV=$((STRESS_RECV + COUNT)) +done + +T_END=$(date +%s) +STRESS_REAL=$((T_END - T_START)) +STRESS_SEND_RPS=$((STRESS_SEND / (STRESS_REAL + 1))) +STRESS_RECV_RPS=$((STRESS_RECV / (STRESS_REAL + 1))) +echo " Duration: ${STRESS_REAL}s" +echo " Sent: ${STRESS_SEND} (${STRESS_SEND_RPS} msg/s)" +echo " Received: ${STRESS_RECV} (${STRESS_RECV_RPS} msg/s)" +echo " Errors: ${STRESS_ERR}" + +sqs_call "Action=DeleteQueue&QueueUrl=${SQS_BASE}/test001/${Q5}" >/dev/null +echo "" + +# ═══════════════════════════════════════════ +# ТЕСТ 6: Большие сообщения +# ═══════════════════════════════════════════ +echo "── ТЕСТ 6: Размер сообщений ──" + +Q6="loadq-size-${TS}" +sqs_call "Action=CreateQueue&QueueName=${Q6}" >/dev/null + +for SIZE in 1 1024 10240 65536 131072 262144; do + BODY=$(python3 -c "print('X' * ${SIZE})" 2>/dev/null) + T1=$(ms_now) + R=$(curl -s --max-time 30 -u "$AUTH" -X POST "${SQS_BASE}?Action=SendMessage&QueueUrl=${SQS_BASE}/test001/${Q6}&Version=${VERSION}" --data-urlencode "MessageBody=${BODY}" 2>&1) + T2=$(ms_now) + LAT=$((T2 - T1)) + if echo "$R" | grep -q "MessageId"; then + echo " ${SIZE} bytes: ✅ OK (${LAT}ms)" + else + ERR_TYPE=$(echo "$R" | grep -oP '(?<=)[^<]+' | head -1) + echo " ${SIZE} bytes: ❌ REJECTED — ${ERR_TYPE:-unknown} (${LAT}ms)" + fi +done + +sqs_call "Action=PurgeQueue&QueueUrl=${SQS_BASE}/test001/${Q6}" >/dev/null +sqs_call "Action=DeleteQueue&QueueUrl=${SQS_BASE}/test001/${Q6}" >/dev/null +echo "" + +# ═══════════════════════════════════════════ +# ТЕСТ 7: Целостность данных +# ═══════════════════════════════════════════ +echo "── ТЕСТ 7: Целостность данных (MD5 проверка) ──" + +Q7="loadq-md5-${TS}" +sqs_call "Action=CreateQueue&QueueName=${Q7}" >/dev/null + +MD5_OK=0 +MD5_FAIL=0 +for i in $(seq 1 20); do + MSG="integrity-check-${i}-$(head -c 16 /dev/urandom | base64 | tr -d '/+=')" + EXPECTED_MD5=$(echo -n "$MSG" | md5sum | awk '{print $1}') + + R=$(sqs_call "Action=SendMessage&QueueUrl=${SQS_BASE}/test001/${Q7}&MessageBody=${MSG}") + RETURNED_MD5=$(echo "$R" | grep -oP '(?<=)[^<]+' | head -1) + + if [ "$EXPECTED_MD5" = "$RETURNED_MD5" ]; then + MD5_OK=$((MD5_OK + 1)) + else + MD5_FAIL=$((MD5_FAIL + 1)) + echo " MISMATCH msg=${MSG}: expected=${EXPECTED_MD5} got=${RETURNED_MD5}" + fi +done +echo " MD5 check: ${MD5_OK}/20 OK, ${MD5_FAIL} FAIL" + +sqs_call "Action=DeleteQueue&QueueUrl=${SQS_BASE}/test001/${Q7}" >/dev/null +echo "" + +# ═══════════════════════════════════════════ +# ИТОГО +# ═══════════════════════════════════════════ +echo "╔══════════════════════════════════════════════════════╗" +echo "║ НАГРУЗОЧНЫЙ ТЕСТ ЗАВЕРШЁН ║" +echo "║ $(date '+%Y-%m-%d %H:%M:%S') ║" +echo "╚══════════════════════════════════════════════════════╝" + +# Очистка temp +rm -rf "$TMPDIR" diff --git a/test_harbor_load.sh b/test_harbor_load.sh new file mode 100755 index 0000000..df24969 --- /dev/null +++ b/test_harbor_load.sh @@ -0,0 +1,189 @@ +#!/usr/bin/env bash +# 2026-04-09 +# Нагрузочный тест Harbor: параллельные curl-запросы, замер латентности, +# фиксация зависаний по таймауту. Позволяет воспроизвести и измерить +# intermittent-проблему с HTTP-протоколом на конкретном хосте. + +set -uo pipefail + +# ── Конфигурация ───────────────────────────────────────────────────────────── +SECRETS_FILE="${SECRETS_FILE:-secrets/pearlharbor_registry.txt}" +DURATION=${DURATION:-60} # секунд +CONCURRENCY=${CONCURRENCY:-10} # параллельных воркеров +TIMEOUT=${TIMEOUT:-8} # таймаут одного запроса (сек) +LOGDIR="${LOGDIR:-/tmp/harbor_load_$(date +%Y%m%d_%H%M%S)}" + +# ── Читаем секреты ──────────────────────────────────────────────────────────── +if [ ! -f "$SECRETS_FILE" ]; then + echo "ERROR: secrets file not found: $SECRETS_FILE" >&2 + exit 1 +fi +connection_url=$(grep -E '^connection_url=' "$SECRETS_FILE" | cut -d'=' -f2-) +admin_pass=$(grep -E '^admin_pass=' "$SECRETS_FILE" | cut -d'=' -f2-) +REGISTRY=$(echo "$connection_url" | sed -E 's~https?://~~' | sed -E 's~/$~~') +BASE_URL="https://$REGISTRY" + +# ── Точки нагрузки (URL + принудительный протокол) ─────────────────────────── +# Цель: нагрузить Harbor разными типами запросов и обоими протоколами, +# чтобы поймать и зафиксировать intermittent-зависания. +declare -a TARGETS=( + "h2 $BASE_URL/api/v2.0/ping" + "h1 $BASE_URL/api/v2.0/ping" + "h2 $BASE_URL/api/v2.0/projects" + "h1 $BASE_URL/api/v2.0/projects" + "h2 $BASE_URL/v2/" + "h1 $BASE_URL/v2/" +) + +mkdir -p "$LOGDIR" +RESULT_LOG="$LOGDIR/results.csv" +echo "ts,worker,proto,url,http_code,time_total,timeout" > "$RESULT_LOG" + +echo "=== Harbor Load Test ===" +echo "Registry : $BASE_URL" +echo "Duration : ${DURATION}s" +echo "Workers : $CONCURRENCY" +echo "Timeout : ${TIMEOUT}s per request" +echo "Log : $LOGDIR" +echo "" + +END_TIME=$(( $(date +%s) + DURATION )) + +# ── Один воркер ────────────────────────────────────────────────────────────── +harbor_worker() { + local wid="$1" + local target_idx=$(( wid % ${#TARGETS[@]} )) + local entry="${TARGETS[$target_idx]}" + local proto=$(echo "$entry" | awk '{print $1}') + local url=$(echo "$entry" | awk '{print $2}') + + local proto_flag="" + if [ "$proto" = "h1" ]; then + proto_flag="--http1.1" + else + proto_flag="--http2" + fi + + while [ "$(date +%s)" -lt "$END_TIME" ]; do + local ts + ts=$(date +%s%3N) + # Замеряем время ответа и HTTP-код + local result + result=$(curl -sk $proto_flag \ + --max-time "$TIMEOUT" \ + -u "admin:$admin_pass" \ + -o /dev/null \ + -w "%{http_code} %{time_total}" \ + "$url" 2>/dev/null) || result="000 $TIMEOUT" + + local http_code time_total timed_out + http_code=$(echo "$result" | awk '{print $1}') + time_total=$(echo "$result" | awk '{print $2}') + + # Считаем тайм-аут если http_code=000 или время близко к лимиту + if [ "$http_code" = "000" ]; then + timed_out=1 + else + timed_out=0 + fi + + echo "$ts,$wid,$proto,$url,$http_code,$time_total,$timed_out" >> "$RESULT_LOG" + done +} + +export -f harbor_worker +export END_TIME TIMEOUT LOGDIR RESULT_LOG admin_pass +export -a TARGETS + +# ── Запускаем воркеров параллельно ─────────────────────────────────────────── +echo "Starting $CONCURRENCY workers for ${DURATION}s..." +PIDS=() +for i in $(seq 0 $(( CONCURRENCY - 1 ))); do + harbor_worker "$i" & + PIDS+=($!) +done + +# ── Прогресс каждые 5 секунд ───────────────────────────────────────────────── +while [ "$(date +%s)" -lt "$END_TIME" ]; do + sleep 5 + local_count=$(wc -l < "$RESULT_LOG") + local_timeouts=$(grep -c ",1$" "$RESULT_LOG" 2>/dev/null || echo 0) + elapsed=$(( DURATION - ( END_TIME - $(date +%s) ) )) + echo "[+${elapsed}s] requests: $(( local_count - 1 )), timeouts: $local_timeouts" +done + +# ── Ждём завершения воркеров ───────────────────────────────────────────────── +for pid in "${PIDS[@]}"; do + wait "$pid" 2>/dev/null || true +done + +echo "" +echo "=== Results ===" + +# ── Статистика через python3 ───────────────────────────────────────────────── +python3 - "$RESULT_LOG" << 'PYEOF' +import sys, csv +from collections import defaultdict + +log_file = sys.argv[1] +rows = [] +with open(log_file) as f: + reader = csv.DictReader(f) + for r in reader: + rows.append(r) + +total = len(rows) +timeouts = sum(1 for r in rows if r['timeout'] == '1') +success = sum(1 for r in rows if r['timeout'] == '0' and r['http_code'].startswith('2')) + +times = [float(r['time_total']) for r in rows if r['timeout'] == '0'] +times.sort() + +def pct(lst, p): + if not lst: return 0 + idx = int(len(lst) * p / 100) + return lst[min(idx, len(lst)-1)] + +print(f"Total requests : {total}") +print(f"Success (2xx) : {success} ({100*success//total if total else 0}%)") +print(f"Timeouts : {timeouts} ({100*timeouts//total if total else 0}%)") +other = total - success - timeouts +print(f"Other errors : {other}") +if times: + print(f"Latency (ok) : min={min(times):.3f}s median={pct(times,50):.3f}s p95={pct(times,95):.3f}s max={max(times):.3f}s") + +print() +print("--- By protocol ---") +by_proto = defaultdict(lambda: {'ok':0,'fail':0,'times':[]}) +for r in rows: + p = r['proto'] + if r['timeout'] == '1': + by_proto[p]['fail'] += 1 + elif r['http_code'].startswith('2'): + by_proto[p]['ok'] += 1 + by_proto[p]['times'].append(float(r['time_total'])) + else: + by_proto[p]['fail'] += 1 + +for proto, d in sorted(by_proto.items()): + t = sorted(d['times']) + p95 = pct(t, 95) if t else 0 + print(f" {proto}: ok={d['ok']} fail={d['fail']} p95={p95:.3f}s") + +print() +print("--- By URL ---") +by_url = defaultdict(lambda: {'ok':0,'to':0}) +for r in rows: + u = r['url'] + if r['timeout'] == '1': + by_url[u]['to'] += 1 + elif r['http_code'].startswith('2'): + by_url[u]['ok'] += 1 + # else: counted as non-2xx + +for url, d in sorted(by_url.items(), key=lambda x: x[0]): + print(f" {url}: ok={d['ok']} timeout={d['to']}") +PYEOF + +echo "" +echo "Raw log: $RESULT_LOG"