0
This commit is contained in:
parent
9c7d634986
commit
014c14af5e
650
POSTGRES/bug_hunter.sh
Normal file
650
POSTGRES/bug_hunter.sh
Normal file
@ -0,0 +1,650 @@
|
||||
#!/usr/bin/env bash
|
||||
# 2026-03-21 — bug_hunter.sh: охота за багами во всех POSTGRES-функциях.
|
||||
# Цель: найти bugs типа "ложный 200", "должен 500 но 200", неверные данные.
|
||||
# Логика принципиально отличается от chaos_marathon — здесь акцент на семантике и data integrity.
|
||||
# Запускать ТОЛЬКО на VM через SSH.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
BASE_URL="${BASE_URL:-https://sless.kube5s.ru}"
|
||||
NAMESPACE="${NAMESPACE:-sless-ffd1f598c169b0ae}"
|
||||
TOKEN_FILE="${TOKEN_FILE:-$HOME/terra/sless/test.token}"
|
||||
TOKEN=$(cat "$TOKEN_FILE")
|
||||
|
||||
RED="\033[0;31m"; GREEN="\033[0;32m"; YELLOW="\033[1;33m"; CYAN="\033[0;36m"; NC="\033[0m"
|
||||
|
||||
PASS=0; FAIL=0; TOTAL=0
|
||||
BUGS_FOUND=()
|
||||
|
||||
ts() { date '+%H:%M:%S'; }
|
||||
|
||||
# ── Хелперы ──────────────────────────────────────────────────────────────────
|
||||
raw() {
|
||||
local svc="$1" payload="$2"
|
||||
curl -sf -X POST -H "Content-Type: application/json" \
|
||||
-d "$payload" "${BASE_URL}/fn/${NAMESPACE}/${svc}" 2>/dev/null || echo "__CURL_FAIL__"
|
||||
}
|
||||
|
||||
http_code() {
|
||||
local svc="$1" payload="$2"
|
||||
curl -s -o /dev/null -w "%{http_code}" -X POST -H "Content-Type: application/json" \
|
||||
-d "$payload" "${BASE_URL}/fn/${NAMESPACE}/${svc}" 2>/dev/null || echo "000"
|
||||
}
|
||||
|
||||
# Проверяем что HTTP-код РАВЕН ожидаемому
|
||||
check_http() {
|
||||
local label="$1" got="$2" want="$3"
|
||||
TOTAL=$((TOTAL+1))
|
||||
if [[ "$got" == "$want" ]]; then
|
||||
echo -e "$(ts) ${GREEN}PASS${NC} [$TOTAL] $label"
|
||||
PASS=$((PASS+1))
|
||||
else
|
||||
echo -e "$(ts) ${RED}FAIL${NC} [$TOTAL] $label (got HTTP $got, want HTTP $want)"
|
||||
FAIL=$((FAIL+1))
|
||||
BUGS_FOUND+=("$label → HTTP $got ≠ $want")
|
||||
fi
|
||||
}
|
||||
|
||||
# Проверяем что JSON содержит строку
|
||||
check_has() {
|
||||
local label="$1" body="$2" substr="$3"
|
||||
TOTAL=$((TOTAL+1))
|
||||
if echo "$body" | grep -q "$substr" 2>/dev/null; then
|
||||
echo -e "$(ts) ${GREEN}PASS${NC} [$TOTAL] $label"
|
||||
PASS=$((PASS+1))
|
||||
else
|
||||
echo -e "$(ts) ${RED}FAIL${NC} [$TOTAL] $label (key/substr '$substr' not found in: ${body:0:120})"
|
||||
FAIL=$((FAIL+1))
|
||||
BUGS_FOUND+=("$label → '$substr' not in response")
|
||||
fi
|
||||
}
|
||||
|
||||
# Проверяем что JSON НЕ содержит строку
|
||||
check_not() {
|
||||
local label="$1" body="$2" substr="$3"
|
||||
TOTAL=$((TOTAL+1))
|
||||
if echo "$body" | grep -q "$substr" 2>/dev/null; then
|
||||
echo -e "$(ts) ${RED}FAIL${NC} [$TOTAL] $label (found '$substr' but should NOT be there: ${body:0:120})"
|
||||
FAIL=$((FAIL+1))
|
||||
BUGS_FOUND+=("$label → '$substr' should NOT be in response")
|
||||
else
|
||||
echo -e "$(ts) ${GREEN}PASS${NC} [$TOTAL] $label"
|
||||
PASS=$((PASS+1))
|
||||
fi
|
||||
}
|
||||
|
||||
# Проверяем числовое равенство: jq вытаскивает значение и сравниваем
|
||||
check_val() {
|
||||
local label="$1" body="$2" jq_expr="$3" want="$4"
|
||||
local got
|
||||
got=$(echo "$body" | python3 -c "
|
||||
import json,sys
|
||||
try:
|
||||
d=json.load(sys.stdin)
|
||||
expr='$jq_expr'.lstrip('.')
|
||||
parts=expr.split('.')
|
||||
val=d
|
||||
for p in parts:
|
||||
val=val[p] if isinstance(val,dict) else val[int(p)]
|
||||
print(val)
|
||||
except Exception as e:
|
||||
print('__ERR__:'+str(e))
|
||||
" 2>/dev/null || echo "__ERR__")
|
||||
TOTAL=$((TOTAL+1))
|
||||
if [[ "$got" == "$want" ]]; then
|
||||
echo -e "$(ts) ${GREEN}PASS${NC} [$TOTAL] $label"
|
||||
PASS=$((PASS+1))
|
||||
else
|
||||
echo -e "$(ts) ${RED}FAIL${NC} [$TOTAL] $label (got '$got', want '$want')"
|
||||
FAIL=$((FAIL+1))
|
||||
BUGS_FOUND+=("$label → got '$got' want '$want'")
|
||||
fi
|
||||
}
|
||||
|
||||
# Проверяем что числовое значение >= порога
|
||||
check_gte() {
|
||||
local label="$1" got="$2" min_val="$3"
|
||||
TOTAL=$((TOTAL+1))
|
||||
if [[ "$got" =~ ^[0-9]+$ ]] && (( got >= min_val )); then
|
||||
echo -e "$(ts) ${GREEN}PASS${NC} [$TOTAL] $label (got $got >= $min_val)"
|
||||
PASS=$((PASS+1))
|
||||
else
|
||||
echo -e "$(ts) ${RED}FAIL${NC} [$TOTAL] $label (got '$got', want >= $min_val)"
|
||||
FAIL=$((FAIL+1))
|
||||
BUGS_FOUND+=("$label → $got < $min_val")
|
||||
fi
|
||||
}
|
||||
|
||||
section() {
|
||||
echo ""
|
||||
echo -e "${CYAN}════════════════════════════════════════════════════════════${NC}"
|
||||
echo -e "${CYAN} $1${NC}"
|
||||
echo -e "${CYAN}════════════════════════════════════════════════════════════${NC}"
|
||||
}
|
||||
|
||||
echo -e "${YELLOW}"
|
||||
echo "╔══════════════════════════════════════════════════════════╗"
|
||||
echo "║ BUG HUNTER — $(date '+%Y-%m-%d %H:%M:%S') ║"
|
||||
echo "║ Ищем: ложные 200, неверные данные, скрытые баги ║"
|
||||
echo "╚══════════════════════════════════════════════════════════╝"
|
||||
echo -e "${NC}"
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
section "БЛОК 1 — Тест на Missing Input Validation (должно быть 200, НО БУДЕТ 500 если баг есть)"
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
echo " Каждая функция должна СТОЙКО обрабатывать строку вместо числа."
|
||||
echo " Если возвращает 500 — это BUG: не хватает try/except."
|
||||
|
||||
c=$(http_code "chaos-slowquery" '{"sleep_sec": "не_число"}')
|
||||
check_http "BUG1: chaos-slowquery sleep_sec=string → должен 200" "$c" "200"
|
||||
|
||||
c=$(http_code "chaos-bigpayload" '{"size_kb": "не_число"}')
|
||||
check_http "BUG2: chaos-bigpayload size_kb=string → должен 200" "$c" "200"
|
||||
|
||||
c=$(http_code "py-retry-writer" '{"n": "не_число"}')
|
||||
check_http "BUG3: py-retry-writer n=string → должен 200" "$c" "200"
|
||||
|
||||
c=$(http_code "pg-delete-old" '{"older_than_min": "не_число"}')
|
||||
check_http "BUG4: pg-delete-old older_than_min=string → должен 200" "$c" "200"
|
||||
|
||||
c=$(http_code "chaos-slowquery" '{"sleep_sec": null}')
|
||||
check_http "BUG5: chaos-slowquery sleep_sec=null → должен 200" "$c" "200"
|
||||
|
||||
c=$(http_code "chaos-bigpayload" '{"size_kb": null}')
|
||||
check_http "BUG6: chaos-bigpayload size_kb=null → должен 200" "$c" "200"
|
||||
|
||||
c=$(http_code "chaos-bigpayload" '{"size_kb": -999}')
|
||||
check_http "BUG7: chaos-bigpayload size_kb=-999 (negative) → должен 200" "$c" "200"
|
||||
|
||||
c=$(http_code "chaos-slowquery" '{"sleep_sec": -5}')
|
||||
check_http "BUG8: chaos-slowquery sleep_sec=-5 → должен 200" "$c" "200"
|
||||
|
||||
c=$(http_code "chaos-slowquery" '{"sleep_sec": 99999}')
|
||||
check_http "BUG9: chaos-slowquery sleep_sec=99999 (huge) → должен 200" "$c" "200"
|
||||
|
||||
c=$(http_code "py-retry-writer" '{"n": -1}')
|
||||
check_http "BUG10: py-retry-writer n=-1 (negative) → должен 200" "$c" "200"
|
||||
|
||||
c=$(http_code "py-retry-writer" '{"n": 99999}')
|
||||
check_http "BUG11: py-retry-writer n=99999 (huge, capped) → должен 200" "$c" "200"
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
section "БЛОК 2 — Data Integrity: реальное значение vs ожидаемое"
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
# js-pg-batch: n=0 должен вернуть inserted=0, а не 20 (баг: 0||20=20)
|
||||
r=$(raw "js-pg-batch" '{"n": 0, "prefix": "bughunt-zero"}')
|
||||
check_val "BUG12: js-pg-batch n=0 → inserted должен быть 0" "$r" ".inserted" "0"
|
||||
|
||||
# js-pg-batch: n=1 → ровно 1 строка
|
||||
r=$(raw "js-pg-batch" '{"n": 1, "prefix": "bughunt-one"}')
|
||||
check_val "SAFE: js-pg-batch n=1 → inserted=1" "$r" ".inserted" "1"
|
||||
|
||||
# js-pg-batch: n=200 (cap) → не больше 200
|
||||
r=$(raw "js-pg-batch" '{"n": 9999, "prefix": "bughunt-cap"}')
|
||||
i=$(echo "$r" | python3 -c "import json,sys; print(json.load(sys.stdin)['inserted'])" 2>/dev/null || echo "-1")
|
||||
check_gte "SAFE: js-pg-batch n=9999 → capped (inserted > 0)" "$i" 1
|
||||
# inserted должно быть <= 200
|
||||
TOTAL=$((TOTAL+1))
|
||||
if (( i <= 200 )); then
|
||||
echo -e "$(ts) ${GREEN}PASS${NC} [$TOTAL] SAFE: js-pg-batch n=9999 → capped <= 200 (got $i)"
|
||||
PASS=$((PASS+1))
|
||||
else
|
||||
echo -e "$(ts) ${RED}FAIL${NC} [$TOTAL] BUG: js-pg-batch n=9999 → вставил $i строк (ожидали <= 200)"
|
||||
FAIL=$((FAIL+1))
|
||||
BUGS_FOUND+=("js-pg-batch n=9999 → inserted=$i > 200")
|
||||
fi
|
||||
|
||||
# pg-bulk-insert: точное соответствие n → inserted
|
||||
for n_val in 1 10 100 499 500; do
|
||||
r=$(raw "pg-bulk-insert" "{\"n\": $n_val, \"prefix\": \"bughunt-n$n_val\"}")
|
||||
check_val "SAFE: pg-bulk-insert n=$n_val → inserted=$n_val" "$r" ".inserted" "$n_val"
|
||||
done
|
||||
|
||||
# py-retry-writer с simulate_error: должен вернуть ok:true (retry работает)
|
||||
r=$(raw "py-retry-writer" '{"n": 5, "simulate_error": true, "prefix": "bughunt-retry"}')
|
||||
check_has "SAFE: py-retry-writer simulate_error=true → ok:true" "$r" '"ok": true'
|
||||
attempts=$(echo "$r" | python3 -c "import json,sys; print(json.load(sys.stdin).get('attempts',0))" 2>/dev/null || echo "0")
|
||||
check_gte "SAFE: py-retry-writer simulate_error=true → attempts >= 2" "$attempts" 2
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
section "БЛОК 3 — Semantic / Logic Bugs (ложный 200, неверные данные)"
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
# BUG: pg-search с "_" в query — _ это LIKE wildcard, не экранируется.
|
||||
# Вставляем уникальную строку без подчёркивания, ищем с _ → не должна найтись.
|
||||
UNIQUE_NOUNDERSCORE="bughunt-no-underscore-$(date +%s%N)"
|
||||
raw "pg-bulk-insert" "{\"n\": 1, \"prefix\": \"$UNIQUE_NOUNDERSCORE\"}" >/dev/null
|
||||
# Поиск exact строки — должна найтись
|
||||
r=$(raw "pg-search" "{\"query\": \"$UNIQUE_NOUNDERSCORE\", \"limit\": 10}")
|
||||
cnt=$(echo "$r" | python3 -c "import json,sys; print(json.load(sys.stdin).get('count',0))" 2>/dev/null || echo "0")
|
||||
check_gte "SAFE: pg-search exact match найдена" "$cnt" 1
|
||||
|
||||
# Теперь создаём строку с дефисом в mid позиции: "aXb" где X — любой char.
|
||||
# Ищем с _ (wildcard): "a_b" должно совпадать. Это НЕ баг если мы ищем wildcard.
|
||||
# Реальный баг: ESCAPE не поддерживается, пользователь не может искать literal "_".
|
||||
# Проверяем: строка "test-literal-underscore_here" ищем по query="literal_underscore_here"
|
||||
# Без экраниравания: _ = любой символ → совпадёт AND с "literaXunderscoreYhere" тоже.
|
||||
EXACT_TITLE="bughunt-exact-$(date +%s%N)"
|
||||
raw "pg-upsert" "{\"title\": \"${EXACT_TITLE}\"}" >/dev/null
|
||||
# Поиск с подчёркиванием вместо дефиса в этой строке — совпадать НЕ должно с точным матчем
|
||||
# но совпадёт из-за ILIKE wildcard `_`
|
||||
UNDER_QUERY="${EXACT_TITLE//-/_}" # заменяем дефисы на подчёркивания
|
||||
r=$(raw "pg-search" "{\"query\": \"$UNDER_QUERY\", \"limit\": 100}")
|
||||
underscore_count=$(echo "$r" | python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('count',0))" 2>/dev/null || echo "0")
|
||||
# Если count >> 1, значит _ матчит что попало (wildcard)
|
||||
echo " pg-search с query='${UNDER_QUERY:0:30}...' вернул $underscore_count строк (если > 1 — это баг wildcard)"
|
||||
TOTAL=$((TOTAL+1))
|
||||
# Должен найти ТОЛЬКО нашу строку (count=1), но без экранирования найдёт много
|
||||
if (( underscore_count == 1 )); then
|
||||
echo -e "$(ts) ${GREEN}PASS${NC} [$TOTAL] SAFE: pg-search underscore matches only exact row"
|
||||
PASS=$((PASS+1))
|
||||
else
|
||||
echo -e "$(ts) ${RED}FAIL${NC} [$TOTAL] BUG13: pg-search '_' is unescaped LIKE wildcard → matches $underscore_count rows instead of 1"
|
||||
FAIL=$((FAIL+1))
|
||||
BUGS_FOUND+=("pg-search: '_' is unescaped LIKE wildcard (got $underscore_count rows)")
|
||||
fi
|
||||
|
||||
# BUG: pg-delete-old — параметр НАЗЫВАЕТСЯ older_than_min, но в chaos_marathon шлём older_than_minutes.
|
||||
# Шлём неправильный ключ и правильный — результаты должны различаться.
|
||||
# older_than_minutes=1 → функция игнорирует, использует default (60 мин) → ничего не удалится из только что вставленных
|
||||
# older_than_min=0 → capped to 1 → удалит строки старше 1 мин (ну, только что вставленные > 1 мин назад)
|
||||
# Проверяем: older_than_minutes=0 → использует default 60 → параметр проигнорирован → bug
|
||||
|
||||
# Вставляем свежую строку, пытаемся удалить с older_than_minutes=0 (неверный ключ)
|
||||
FRESH_TITLE="bughunt-del-$(date +%s%N)"
|
||||
ins_r=$(raw "pg-bulk-insert" "{\"n\": 3, \"prefix\": \"$FRESH_TITLE\"}")
|
||||
# Ждём немного, затем пытаемся удалить по неправильному ключу
|
||||
sleep 1
|
||||
r=$(raw "pg-delete-old" "{\"older_than_minutes\": 0}")
|
||||
deleted_wrong_key=$(echo "$r" | python3 -c "import json,sys; print(json.load(sys.stdin).get('deleted',0))" 2>/dev/null || echo "?")
|
||||
r2=$(raw "pg-delete-old" "{\"older_than_min\": 99999}")
|
||||
deleted_right_key=$(echo "$r2" | python3 -c "import json,sys; print(json.load(sys.stdin).get('deleted',0))" 2>/dev/null || echo "?")
|
||||
echo " pg-delete-old older_than_minutes=0: deleted=$deleted_wrong_key (использовался default 60min)"
|
||||
echo " pg-delete-old older_than_min=99999: deleted=$deleted_right_key (правильный ключ — удалило всё старое)"
|
||||
# Если с неверным ключом deleted > 0 при очень маленьком значении — значит параметр работает.
|
||||
# Если с right key deleted больше — это подтверждает что wrong key игнорировался.
|
||||
TOTAL=$((TOTAL+1))
|
||||
if [[ "$deleted_right_key" =~ ^[0-9]+$ ]] && (( deleted_right_key >= 0 )); then
|
||||
echo -e "$(ts) ${GREEN}PASS${NC} [$TOTAL] INFO: pg-delete-old right key works (deleted=$deleted_right_key)"
|
||||
PASS=$((PASS+1))
|
||||
else
|
||||
echo -e "$(ts) ${RED}FAIL${NC} [$TOTAL] pg-delete-old right key failed: got $deleted_right_key"
|
||||
FAIL=$((FAIL+1))
|
||||
fi
|
||||
|
||||
# BUG: pg-counter prefix="%" → LIKE "%%%" → считает все строки (wildcard leak)
|
||||
r_all=$(raw "pg-counter" '{}')
|
||||
total_all=$(echo "$r_all" | python3 -c "import json,sys; print(json.load(sys.stdin).get('total',0))" 2>/dev/null || echo "0")
|
||||
r_pct=$(raw "pg-counter" '{"prefix": "%"}')
|
||||
total_pct=$(echo "$r_pct" | python3 -c "import json,sys; print(json.load(sys.stdin).get('total',0))" 2>/dev/null || echo "0")
|
||||
echo " pg-counter prefix='': total=$total_all | pg-counter prefix='%': total=$total_pct"
|
||||
TOTAL=$((TOTAL+1))
|
||||
# Если оба возвращают одно число — значит % матчит все строки → баг wildcard
|
||||
if [[ "$total_all" == "$total_pct" ]] && (( total_all > 0 )); then
|
||||
echo -e "$(ts) ${RED}FAIL${NC} [$TOTAL] BUG14: pg-counter prefix='%' counts ALL rows ($total_pct) — % is unescaped LIKE wildcard"
|
||||
FAIL=$((FAIL+1))
|
||||
BUGS_FOUND+=("pg-counter: prefix='%' is unescaped LIKE wildcard (same as no prefix)")
|
||||
else
|
||||
echo -e "$(ts) ${GREEN}PASS${NC} [$TOTAL] SAFE: pg-counter prefix='%' gives different count than no-prefix"
|
||||
PASS=$((PASS+1))
|
||||
fi
|
||||
|
||||
# BUG: pg-search limit=0 — clamp max(1, min(0,100)) = 1, не 0.
|
||||
# Юзер просит 0 строк но получает 1.
|
||||
r=$(raw "pg-search" '{"query": "", "limit": 0}')
|
||||
limit_got=$(echo "$r" | python3 -c "import json,sys; print(json.load(sys.stdin).get('limit',0))" 2>/dev/null || echo "-1")
|
||||
echo " pg-search limit=0 → вернул limit=$limit_got в ответе"
|
||||
TOTAL=$((TOTAL+1))
|
||||
if [[ "$limit_got" == "0" ]]; then
|
||||
echo -e "$(ts) ${GREEN}PASS${NC} [$TOTAL] SAFE: pg-search limit=0 → limit=0 in response"
|
||||
PASS=$((PASS+1))
|
||||
else
|
||||
echo -e "$(ts) ${RED}FAIL${NC} [$TOTAL] INFO15: pg-search limit=0 → silently changed to $limit_got (min clamp = 1, not 0)"
|
||||
FAIL=$((FAIL+1))
|
||||
BUGS_FOUND+=("pg-search: limit=0 silently becomes $limit_got (user wants 0 rows, gets $limit_got)")
|
||||
fi
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
section "БЛОК 4 — pg-upsert: idempotency + action field correctness"
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
# Первый вызов → action=inserted
|
||||
UPSERT_KEY="bughunt-upsert-$(date +%s%N)"
|
||||
r1=$(raw "pg-upsert" "{\"title\": \"$UPSERT_KEY\"}")
|
||||
action1=$(echo "$r1" | python3 -c "import json,sys; print(json.load(sys.stdin).get('action','?'))" 2>/dev/null || echo "?")
|
||||
check_val "SAFE: pg-upsert первый вызов → action=inserted" "$r1" ".action" "inserted"
|
||||
|
||||
# Второй вызов → action=updated
|
||||
r2=$(raw "pg-upsert" "{\"title\": \"$UPSERT_KEY\"}")
|
||||
action2=$(echo "$r2" | python3 -c "import json,sys; print(json.load(sys.stdin).get('action','?'))" 2>/dev/null || echo "?")
|
||||
check_val "SAFE: pg-upsert второй вызов (same title) → action=updated" "$r2" ".action" "updated"
|
||||
|
||||
# ID должен совпадать
|
||||
id1=$(echo "$r1" | python3 -c "import json,sys; print(json.load(sys.stdin).get('id','?'))" 2>/dev/null || echo "?1")
|
||||
id2=$(echo "$r2" | python3 -c "import json,sys; print(json.load(sys.stdin).get('id','?'))" 2>/dev/null || echo "?2")
|
||||
TOTAL=$((TOTAL+1))
|
||||
if [[ "$id1" == "$id2" ]] && [[ "$id1" != "?" ]]; then
|
||||
echo -e "$(ts) ${GREEN}PASS${NC} [$TOTAL] SAFE: pg-upsert same title → same id ($id1)"
|
||||
PASS=$((PASS+1))
|
||||
else
|
||||
echo -e "$(ts) ${RED}FAIL${NC} [$TOTAL] BUG16: pg-upsert same title → different ids ($id1 vs $id2)"
|
||||
FAIL=$((FAIL+1))
|
||||
BUGS_FOUND+=("pg-upsert: same title yields different ids ($id1 vs $id2)")
|
||||
fi
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
section "БЛОК 5 — js-idempotent: concurrent same key"
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
# 5 параллельных вызовов с одним ключом → должен быть 1 created, 4 existing
|
||||
IDEM_KEY="bughunt-idem-concurrent-$(date +%s%N)"
|
||||
declare -a IDEM_RESULTS=()
|
||||
for i in 1 2 3 4 5; do
|
||||
r=$(raw "js-idempotent" "{\"idempotency_key\": \"$IDEM_KEY\"}") &
|
||||
IDEM_RESULTS+=($!)
|
||||
done
|
||||
wait
|
||||
# Перезапустим последовательно чтобы собрать результаты
|
||||
actions=()
|
||||
for i in 1 2 3 4 5; do
|
||||
r=$(raw "js-idempotent" "{\"idempotency_key\": \"$IDEM_KEY\"}")
|
||||
a=$(echo "$r" | python3 -c "import json,sys; print(json.load(sys.stdin).get('action','?'))" 2>/dev/null || echo "?")
|
||||
actions+=("$a")
|
||||
done
|
||||
|
||||
created_count=0; existing_count=0
|
||||
for a in "${actions[@]}"; do
|
||||
[[ "$a" == "created" ]] && created_count=$((created_count+1))
|
||||
[[ "$a" == "existing" ]] && existing_count=$((existing_count+1))
|
||||
done
|
||||
echo " js-idempotent key же 5× последовательно: created=$created_count, existing=$existing_count"
|
||||
TOTAL=$((TOTAL+1))
|
||||
# Первый должен быть created, остальные existing (с учётом что первый вызов в параллельном блоке уже создал)
|
||||
# Теперь все 5 последовательных должны быть existing
|
||||
if (( existing_count == 5 )); then
|
||||
echo -e "$(ts) ${GREEN}PASS${NC} [$TOTAL] SAFE: js-idempotent 5× same key → все existing"
|
||||
PASS=$((PASS+1))
|
||||
elif (( created_count == 1 && existing_count == 4 )); then
|
||||
echo -e "$(ts) ${GREEN}PASS${NC} [$TOTAL] SAFE: js-idempotent 5× same key → 1 created + 4 existing"
|
||||
PASS=$((PASS+1))
|
||||
else
|
||||
echo -e "$(ts) ${RED}FAIL${NC} [$TOTAL] BUG17: js-idempotent same key → created=$created_count existing=$existing_count (ожидали 0-1 created, 4-5 existing)"
|
||||
FAIL=$((FAIL+1))
|
||||
BUGS_FOUND+=("js-idempotent: 5× same key → created=$created_count, existing=$existing_count")
|
||||
fi
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
section "БЛОК 6 — go-pg-race: workers=0 div-by-zero"
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
r=$(raw "go-pg-race" '{"workers": 0, "n_per_worker": 5}')
|
||||
c=$(http_code "go-pg-race" '{"workers": 0, "n_per_worker": 5}')
|
||||
check_http "SAFE: go-pg-race workers=0 → 200 (не div-by-zero)" "$c" "200"
|
||||
|
||||
# ops_per_sec при workers=0 inserted=0 должен быть 0 или Inf — проверяем что не NaN/invalid JSON
|
||||
TOTAL=$((TOTAL+1))
|
||||
if echo "$r" | python3 -c "import json,sys; json.load(sys.stdin); print('valid_json')" 2>/dev/null | grep -q valid_json; then
|
||||
echo -e "$(ts) ${GREEN}PASS${NC} [$TOTAL] SAFE: go-pg-race workers=0 → valid JSON"
|
||||
PASS=$((PASS+1))
|
||||
else
|
||||
echo -e "$(ts) ${RED}FAIL${NC} [$TOTAL] BUG18: go-pg-race workers=0 → invalid JSON (div-by-zero → Inf/NaN)"
|
||||
FAIL=$((FAIL+1))
|
||||
BUGS_FOUND+=("go-pg-race: workers=0 → invalid JSON response")
|
||||
fi
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
section "БЛОК 7 — Crash functions: параметры управляют crash-ом"
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
# stress-go-nil: crash=false → 200 (не должен падать)
|
||||
c=$(http_code "stress-go-nil" '{"crash": false}')
|
||||
check_http "SAFE: stress-go-nil crash=false → 200" "$c" "200"
|
||||
|
||||
# stress-go-nil: crash=true (default) → 500
|
||||
c=$(http_code "stress-go-nil" '{"crash": true}')
|
||||
check_http "SAFE: stress-go-nil crash=true → 500" "$c" "500"
|
||||
|
||||
# stress-divzero: d=0 → 500
|
||||
c=$(http_code "stress-divzero" '{"n": 10, "d": 0}')
|
||||
check_http "SAFE: stress-divzero d=0 → 500" "$c" "500"
|
||||
|
||||
# stress-divzero: d=2 → 200
|
||||
c=$(http_code "stress-divzero" '{"n": 10, "d": 2}')
|
||||
check_http "SAFE: stress-divzero d=2 → 200" "$c" "200"
|
||||
|
||||
r=$(raw "stress-divzero" '{"n": 10, "d": 2}')
|
||||
check_has "SAFE: stress-divzero d=2 → result in response" "$r" "result"
|
||||
|
||||
# stress-bigloop: n=1000000 (большое) → должен вернуть 200
|
||||
c=$(http_code "stress-bigloop" '{"n": 1000000}')
|
||||
check_http "SAFE: stress-bigloop n=1000000 → 200" "$c" "200"
|
||||
|
||||
# stress-go-fast: n=0 → должен вернуть 200 (factorial(0) = 1)
|
||||
c=$(http_code "stress-go-fast" '{"n": 0}')
|
||||
check_http "SAFE: stress-go-fast n=0 → 200" "$c" "200"
|
||||
|
||||
# stress-go-fast: n=20 (cap) → factorial(20) не переполнение?
|
||||
r=$(raw "stress-go-fast" '{"n": 20}')
|
||||
check_has "SAFE: stress-go-fast n=20 → factorial in response" "$r" "factorial"
|
||||
|
||||
# stress-go-fast: n=21 → capped to 20 (проверяем что cap работает)
|
||||
r=$(raw "stress-go-fast" '{"n": 21}')
|
||||
n_got=$(echo "$r" | python3 -c "import json,sys; print(json.load(sys.stdin).get('n',0))" 2>/dev/null || echo "-1")
|
||||
TOTAL=$((TOTAL+1))
|
||||
if (( n_got <= 20 )); then
|
||||
echo -e "$(ts) ${GREEN}PASS${NC} [$TOTAL] SAFE: stress-go-fast n=21 → capped to $n_got (<= 20)"
|
||||
PASS=$((PASS+1))
|
||||
else
|
||||
echo -e "$(ts) ${RED}FAIL${NC} [$TOTAL] BUG: stress-go-fast n=21 → not capped (n=$n_got)"
|
||||
FAIL=$((FAIL+1))
|
||||
BUGS_FOUND+=("stress-go-fast: n=21 not capped (got n=$n_got)")
|
||||
fi
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
section "БЛОК 8 — pg-counter: счёт соответствует реально inserted"
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
PREFIX_TEST="bughunt-count-$(date +%s%N)"
|
||||
# Получаем текущий счётчик с этим prefix
|
||||
r_before=$(raw "pg-counter" "{\"prefix\": \"$PREFIX_TEST\"}")
|
||||
cnt_before=$(echo "$r_before" | python3 -c "import json,sys; print(json.load(sys.stdin).get('total',0))" 2>/dev/null || echo "0")
|
||||
|
||||
# Вставляем ровно 7 строк
|
||||
raw "pg-bulk-insert" "{\"n\": 7, \"prefix\": \"$PREFIX_TEST\"}" >/dev/null
|
||||
|
||||
# Считаем снова
|
||||
r_after=$(raw "pg-counter" "{\"prefix\": \"$PREFIX_TEST\"}")
|
||||
cnt_after=$(echo "$r_after" | python3 -c "import json,sys; print(json.load(sys.stdin).get('total',0))" 2>/dev/null || echo "0")
|
||||
|
||||
inserted_delta=$(( cnt_after - cnt_before ))
|
||||
echo " pg-counter prefix='$PREFIX_TEST': before=$cnt_before, after=$cnt_after, delta=$inserted_delta (ожидаем 7)"
|
||||
TOTAL=$((TOTAL+1))
|
||||
if (( inserted_delta == 7 )); then
|
||||
echo -e "$(ts) ${GREEN}PASS${NC} [$TOTAL] SAFE: pg-counter правильно считает: delta=$inserted_delta"
|
||||
PASS=$((PASS+1))
|
||||
else
|
||||
echo -e "$(ts) ${RED}FAIL${NC} [$TOTAL] BUG19: pg-counter delta=$inserted_delta ≠ 7 (prefix wildcard или баг в счёте)"
|
||||
FAIL=$((FAIL+1))
|
||||
BUGS_FOUND+=("pg-counter: inserted 7, delta=$inserted_delta")
|
||||
fi
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
section "БЛОК 9 — pg-search: pagination correctness"
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
PREFIX_SEARCH="bughunt-search-$(date +%s%N)"
|
||||
# Вставляем ровно 25 строк
|
||||
raw "pg-bulk-insert" "{\"n\": 25, \"prefix\": \"$PREFIX_SEARCH\"}" >/dev/null
|
||||
sleep 0.5
|
||||
|
||||
# Page 1: offset=0 limit=10 → count=10
|
||||
r=$(raw "pg-search" "{\"query\": \"$PREFIX_SEARCH\", \"limit\": 10, \"offset\": 0}")
|
||||
count_p1=$(echo "$r" | python3 -c "import json,sys; print(json.load(sys.stdin).get('count',0))" 2>/dev/null || echo "-1")
|
||||
total_p1=$(echo "$r" | python3 -c "import json,sys; print(json.load(sys.stdin).get('total',0))" 2>/dev/null || echo "-1")
|
||||
check_val "SAFE: pg-search page1 count=10" "$r" ".count" "10"
|
||||
check_gte "SAFE: pg-search total >= 25" "$total_p1" 25
|
||||
|
||||
# Page 3: offset=20 limit=10 → count=5 (строк 21-25)
|
||||
r=$(raw "pg-search" "{\"query\": \"$PREFIX_SEARCH\", \"limit\": 10, \"offset\": 20}")
|
||||
count_p3=$(echo "$r" | python3 -c "import json,sys; print(json.load(sys.stdin).get('count',0))" 2>/dev/null || echo "-1")
|
||||
echo " pg-search page3 (offset=20, limit=10): count=$count_p3 (ожидаем 5)"
|
||||
TOTAL=$((TOTAL+1))
|
||||
# Учитываем что могут быть другие строки с этим prefix
|
||||
if [[ "$count_p3" =~ ^[0-9]+$ ]] && (( count_p3 > 0 && count_p3 <= 10 )); then
|
||||
echo -e "$(ts) ${GREEN}PASS${NC} [$TOTAL] SAFE: pg-search page3 count=$count_p3 (допустимо)"
|
||||
PASS=$((PASS+1))
|
||||
else
|
||||
echo -e "$(ts) ${RED}FAIL${NC} [$TOTAL] BUG20: pg-search page3 count=$count_p3 (expected 1-10)"
|
||||
FAIL=$((FAIL+1))
|
||||
BUGS_FOUND+=("pg-search: page3 count=$count_p3 out of range")
|
||||
fi
|
||||
|
||||
# offset > total → count=0
|
||||
r=$(raw "pg-search" "{\"query\": \"$PREFIX_SEARCH\", \"limit\": 10, \"offset\": 999999}")
|
||||
count_over=$(echo "$r" | python3 -c "import json,sys; print(json.load(sys.stdin).get('count',0))" 2>/dev/null || echo "-1")
|
||||
check_val "SAFE: pg-search offset>total → count=0" "$r" ".count" "0"
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
section "БЛОК 10 — pg-dedup: idempotency (повторный вызов безопасен)"
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
# Сначала удаляем все дубли
|
||||
raw "pg-dedup" '{"dry_run": false}' >/dev/null
|
||||
|
||||
# dry_run=true → deleted всегда = 0
|
||||
r=$(raw "pg-dedup" '{"dry_run": true}')
|
||||
check_val "SAFE: pg-dedup dry_run=true → deleted=0" "$r" ".deleted" "0"
|
||||
|
||||
# Создаём дублей: вставляем одно и то же через bulk (уникальные title), затем уpsert одно и то же
|
||||
DUP_TITLE="bughunt-dup-$(date +%s%N)"
|
||||
raw "pg-upsert" "{\"title\": \"${DUP_TITLE}\"}" >/dev/null
|
||||
# INSERT прямой дубль через bulk — но у него нет механизма вставки дублей...
|
||||
# Используем pg-counter + pg-search чтобы найти дубли что уже есть от других тестов
|
||||
r=$(raw "pg-dedup" '{"dry_run": true}')
|
||||
dupes=$(echo "$r" | python3 -c "import json,sys; print(json.load(sys.stdin).get('duplicates_found',0))" 2>/dev/null || echo "0")
|
||||
echo " pg-dedup dry_run: duplicates_found=$dupes"
|
||||
|
||||
# Запускаем dedup дважды — второй раз должен найти 0 дублей
|
||||
raw "pg-dedup" '{"dry_run": false}' >/dev/null
|
||||
r2=$(raw "pg-dedup" '{"dry_run": false}')
|
||||
dupes2=$(echo "$r2" | python3 -c "import json,sys; print(json.load(sys.stdin).get('duplicates_found',0))" 2>/dev/null || echo "0")
|
||||
TOTAL=$((TOTAL+1))
|
||||
if [[ "$dupes2" == "0" ]]; then
|
||||
echo -e "$(ts) ${GREEN}PASS${NC} [$TOTAL] SAFE: pg-dedup повторный вызов → duplicates_found=0 (идемпотентен)"
|
||||
PASS=$((PASS+1))
|
||||
else
|
||||
echo -e "$(ts) ${RED}FAIL${NC} [$TOTAL] BUG21: pg-dedup повторный вызов → duplicates_found=$dupes2 ≠ 0"
|
||||
FAIL=$((FAIL+1))
|
||||
BUGS_FOUND+=("pg-dedup: не идемпотентен — второй вызов нашёл $dupes2 дублей")
|
||||
fi
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
section "БЛОК 11 — chaos-echo: крайние случаи"
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
# Пустой JSON {} → echo должен вернуть echo:{}, keys:[], size_bytes:2
|
||||
r=$(raw "chaos-echo" '{}')
|
||||
check_has "SAFE: chaos-echo {} → echo in response" "$r" '"echo"'
|
||||
check_val "SAFE: chaos-echo {} → keys=[](empty_list len=0)" "$r" ".size_bytes" "2"
|
||||
|
||||
# Очень глубоко вложенный JSON
|
||||
r=$(raw "chaos-echo" '{"a": {"b": {"c": {"d": {"e": "deep"}}}}}')
|
||||
c=$(http_code "chaos-echo" '{"a": {"b": {"c": {"d": {"e": "deep"}}}}}')
|
||||
check_http "SAFE: chaos-echo deeply nested → 200" "$c" "200"
|
||||
|
||||
# Массив вместо объекта (некоторые функции падают)
|
||||
c=$(http_code "chaos-echo" '[1, 2, 3]')
|
||||
check_http "SAFE: chaos-echo array input → 200" "$c" "200"
|
||||
|
||||
# Булево значение вместо объекта
|
||||
c=$(http_code "chaos-echo" 'true')
|
||||
check_http "SAFE: chaos-echo true input → 200" "$c" "200"
|
||||
|
||||
# Число вместо объекта
|
||||
c=$(http_code "chaos-echo" '42')
|
||||
check_http "SAFE: chaos-echo number input → 200" "$c" "200"
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
section "БЛОК 12 — go-counter-atomic: invocation_n растёт"
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
r1=$(raw "go-counter-atomic" '{}')
|
||||
n1=$(echo "$r1" | python3 -c "import json,sys; print(json.load(sys.stdin).get('invocation_n','?'))" 2>/dev/null || echo "?")
|
||||
r2=$(raw "go-counter-atomic" '{}')
|
||||
n2=$(echo "$r2" | python3 -c "import json,sys; print(json.load(sys.stdin).get('invocation_n','?'))" 2>/dev/null || echo "?")
|
||||
echo " go-counter-atomic: call1 invocation_n=$n1, call2 invocation_n=$n2"
|
||||
TOTAL=$((TOTAL+1))
|
||||
if [[ "$n1" =~ ^[0-9]+$ ]] && [[ "$n2" =~ ^[0-9]+$ ]] && (( n2 > n1 )); then
|
||||
echo -e "$(ts) ${GREEN}PASS${NC} [$TOTAL] SAFE: go-counter-atomic invocation_n растёт ($n1 → $n2)"
|
||||
PASS=$((PASS+1))
|
||||
else
|
||||
echo -e "$(ts) ${RED}FAIL${NC} [$TOTAL] BUG22: go-counter-atomic invocation_n НЕ растёт ($n1 → $n2)"
|
||||
FAIL=$((FAIL+1))
|
||||
BUGS_FOUND+=("go-counter-atomic: invocation_n не растёт ($n1 → $n2)")
|
||||
fi
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
section "БЛОК 13 — go-pg-race: все inserted = workers × n_per_worker"
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
r=$(raw "go-pg-race" '{"workers": 4, "n_per_worker": 10}')
|
||||
ins=$(echo "$r" | python3 -c "import json,sys; print(json.load(sys.stdin).get('inserted',0))" 2>/dev/null || echo "0")
|
||||
errs=$(echo "$r" | python3 -c "import json,sys; print(json.load(sys.stdin).get('errors',0))" 2>/dev/null || echo "-1")
|
||||
echo " go-pg-race workers=4 n_per_worker=10: inserted=$ins, errors=$errs (ожидаем inserted=40, errors=0)"
|
||||
TOTAL=$((TOTAL+1))
|
||||
if [[ "$ins" == "40" ]] && [[ "$errs" == "0" ]]; then
|
||||
echo -e "$(ts) ${GREEN}PASS${NC} [$TOTAL] SAFE: go-pg-race 4×10 = 40 inserted, 0 errors"
|
||||
PASS=$((PASS+1))
|
||||
else
|
||||
echo -e "$(ts) ${RED}FAIL${NC} [$TOTAL] BUG23: go-pg-race 4×10: inserted=$ins (≠40), errors=$errs"
|
||||
FAIL=$((FAIL+1))
|
||||
BUGS_FOUND+=("go-pg-race: workers=4 n_per_worker=10 → inserted=$ins errors=$errs")
|
||||
fi
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
section "БЛОК 14 — pg-bulk-insert: first_id реально существует в БД"
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
PREFIX_VER="bughunt-verify-$(date +%s%N)"
|
||||
r=$(raw "pg-bulk-insert" "{\"n\": 5, \"prefix\": \"$PREFIX_VER\"}")
|
||||
first_id=$(echo "$r" | python3 -c "import json,sys; print(json.load(sys.stdin).get('first_id','null'))" 2>/dev/null || echo "null")
|
||||
echo " pg-bulk-insert n=5 → first_id=$first_id"
|
||||
|
||||
# Проверяем что эта строка находится через pg-search
|
||||
sleep 0.3
|
||||
r_search=$(raw "pg-search" "{\"query\": \"$PREFIX_VER\", \"limit\": 10}")
|
||||
found_count=$(echo "$r_search" | python3 -c "import json,sys; print(json.load(sys.stdin).get('count',0))" 2>/dev/null || echo "0")
|
||||
TOTAL=$((TOTAL+1))
|
||||
if (( found_count >= 5 )); then
|
||||
echo -e "$(ts) ${GREEN}PASS${NC} [$TOTAL] SAFE: pg-bulk-insert n=5 → pg-search находит $found_count строк"
|
||||
PASS=$((PASS+1))
|
||||
else
|
||||
echo -e "$(ts) ${RED}FAIL${NC} [$TOTAL] BUG24: pg-bulk-insert n=5 → pg-search нашёл только $found_count строк"
|
||||
FAIL=$((FAIL+1))
|
||||
BUGS_FOUND+=("pg-bulk-insert: inserted 5 but pg-search found only $found_count")
|
||||
fi
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
section "ИТОГИ BUG HUNTER"
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
echo ""
|
||||
echo -e "${YELLOW}PASS: $PASS / $TOTAL FAIL: $FAIL${NC}"
|
||||
echo ""
|
||||
|
||||
if (( ${#BUGS_FOUND[@]} > 0 )); then
|
||||
echo -e "${RED}╔══════════════════════════════════════════════════════════════╗${NC}"
|
||||
echo -e "${RED}║ НАЙДЕНО БАГОВ: ${#BUGS_FOUND[@]} ║${NC}"
|
||||
echo -e "${RED}╚══════════════════════════════════════════════════════════════╝${NC}"
|
||||
for bug in "${BUGS_FOUND[@]}"; do
|
||||
echo -e " ${RED}✗${NC} $bug"
|
||||
done
|
||||
else
|
||||
echo -e "${GREEN}╔══════════════════════════════════════╗${NC}"
|
||||
echo -e "${GREEN}║ БАГОВ НЕ НАЙДЕНО. Всё чисто. ✓ ║${NC}"
|
||||
echo -e "${GREEN}╚══════════════════════════════════════╝${NC}"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
exit $(( FAIL > 0 ? 1 : 0 ))
|
||||
668
POSTGRES/chaos_marathon.sh
Normal file
668
POSTGRES/chaos_marathon.sh
Normal file
@ -0,0 +1,668 @@
|
||||
#!/usr/bin/env bash
|
||||
# 2026-03-21 — chaos_marathon.sh
|
||||
# Часовой хаос-марафон: 15 сервисов, dumb-user simulation, PG stress, CRUD lifecycle.
|
||||
# Запуск: bash chaos_marathon.sh 2>&1 | tee /tmp/chaos_marathon_$(date +%Y%m%d_%H%M).log
|
||||
#
|
||||
# Предполагает: terraform apply chaos_marathon.tf уже выполнен, все 15 Ready.
|
||||
# Зависимости: curl, jq, terraform (init выполнен).
|
||||
|
||||
# -e намеренно НЕ установлен — падение одного вызова не убивает марафон.
|
||||
# -u: незаданные переменные = ошибка. -o pipefail: ошибка в пайпе видна.
|
||||
set -uo pipefail
|
||||
|
||||
# ── Config ────────────────────────────────────────────────────────────────────
|
||||
|
||||
BASE_URL="${SLESS_BASE_URL:-https://sless.kube5s.ru}"
|
||||
TOKEN_FILE="${SLESS_TOKEN_FILE:-/home/naeel/terra/sless/test.token}"
|
||||
NAMESPACE="${SLESS_NAMESPACE:-sless-ffd1f598c169b0ae}"
|
||||
TF_DIR="/home/naeel/terra/sless/examples/POSTGRES"
|
||||
LOG_DIR="/tmp/chaos_$(date +%Y%m%d_%H%M%S)"
|
||||
mkdir -p "$LOG_DIR"
|
||||
|
||||
# ── Helpers ───────────────────────────────────────────────────────────────────
|
||||
|
||||
TOKEN=$(cat "$TOKEN_FILE")
|
||||
PASS=0
|
||||
FAIL=0
|
||||
TOTAL=0
|
||||
|
||||
# Цветной вывод — для удобства чтения длинного лога.
|
||||
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; CYAN='\033[0;36m'; NC='\033[0m'
|
||||
|
||||
ts() { date '+%H:%M:%S'; }
|
||||
|
||||
# invoke SERVICE_NAME PAYLOAD — вызывает сервис через публичный /fn/ proxy, возвращает тело.
|
||||
# Публичный endpoint не требует токена (используется для вызова функций).
|
||||
# Никогда не падает — ошибки пишутся в лог-файл.
|
||||
invoke() {
|
||||
local svc="$1" payload="$2"
|
||||
curl -sf -X POST \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$payload" \
|
||||
"${BASE_URL}/fn/${NAMESPACE}/${svc}" \
|
||||
2>"$LOG_DIR/err_${svc}_$(date +%s%N).log" || true
|
||||
}
|
||||
|
||||
# invoke_raw — как invoke, но никогда не бросает non-zero exit.
|
||||
invoke_raw() {
|
||||
local svc="$1" payload="$2"
|
||||
curl -s -X POST \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$payload" \
|
||||
"${BASE_URL}/fn/${NAMESPACE}/${svc}" || true
|
||||
}
|
||||
|
||||
# invoke_with_status SERVICE PAYLOAD — возвращает HTTP код, никогда не падает.
|
||||
invoke_with_status() {
|
||||
local svc="$1" payload="$2"
|
||||
curl -s -o /dev/null -w "%{http_code}" -X POST \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$payload" \
|
||||
"${BASE_URL}/fn/${NAMESPACE}/${svc}" || echo "000"
|
||||
}
|
||||
|
||||
# check TEST_NAME CONDITION [msg] — вердикт по условию (expect 0-exit или строковую проверку).
|
||||
check() {
|
||||
local name="$1" result="$2" expected="${3:-0}"
|
||||
TOTAL=$((TOTAL + 1))
|
||||
if [[ "$result" == "$expected" ]]; then
|
||||
PASS=$((PASS + 1))
|
||||
echo -e "$(ts) ${GREEN}PASS${NC} [$TOTAL] $name"
|
||||
else
|
||||
FAIL=$((FAIL + 1))
|
||||
echo -e "$(ts) ${RED}FAIL${NC} [$TOTAL] $name (got='$result' want='$expected')"
|
||||
fi
|
||||
}
|
||||
|
||||
# check_contains TEST_NAME HAYSTACK NEEDLE
|
||||
check_contains() {
|
||||
local name="$1" hay="$2" needle="$3"
|
||||
TOTAL=$((TOTAL + 1))
|
||||
if echo "$hay" | grep -q "$needle"; then
|
||||
PASS=$((PASS + 1))
|
||||
echo -e "$(ts) ${GREEN}PASS${NC} [$TOTAL] $name"
|
||||
else
|
||||
FAIL=$((FAIL + 1))
|
||||
echo -e "$(ts) ${RED}FAIL${NC} [$TOTAL] $name (needle='$needle' not found)"
|
||||
fi
|
||||
}
|
||||
|
||||
# check_not_contains TEST_NAME HAYSTACK NEEDLE
|
||||
check_not_contains() {
|
||||
local name="$1" hay="$2" needle="$3"
|
||||
TOTAL=$((TOTAL + 1))
|
||||
if ! echo "$hay" | grep -q "$needle"; then
|
||||
PASS=$((PASS + 1))
|
||||
echo -e "$(ts) ${GREEN}PASS${NC} [$TOTAL] $name"
|
||||
else
|
||||
FAIL=$((FAIL + 1))
|
||||
echo -e "$(ts) ${RED}FAIL${NC} [$TOTAL] $name (unexpected needle='$needle' found)"
|
||||
fi
|
||||
}
|
||||
|
||||
# check_http TEST_NAME CODE EXPECTED
|
||||
check_http() {
|
||||
local name="$1" code="$2" expected="${3:-200}"
|
||||
TOTAL=$((TOTAL + 1))
|
||||
if [[ "$code" == "$expected" ]]; then
|
||||
PASS=$((PASS + 1))
|
||||
echo -e "$(ts) ${GREEN}PASS${NC} [$TOTAL] $name → HTTP $code"
|
||||
else
|
||||
FAIL=$((FAIL + 1))
|
||||
echo -e "$(ts) ${RED}FAIL${NC} [$TOTAL] $name → HTTP $code (want $expected)"
|
||||
fi
|
||||
}
|
||||
|
||||
section() {
|
||||
echo ""
|
||||
echo -e "${CYAN}════════════════════════════════════════════════════════════${NC}"
|
||||
echo -e "${CYAN} $1${NC}"
|
||||
echo -e "${CYAN}════════════════════════════════════════════════════════════${NC}"
|
||||
}
|
||||
|
||||
# ── Wait helpers ──────────────────────────────────────────────────────────────
|
||||
|
||||
# wait_service_ready NAME — ждём до 2 мин пока GET /services/{name} вернёт phase=Ready.
|
||||
# Проверяет статус через API (не invoke), чтобы не запускать функцию при старте.
|
||||
wait_service_ready() {
|
||||
local svc="$1" max_attempts=24 attempt=0
|
||||
echo " Ожидаем готовности $svc..."
|
||||
while (( attempt < max_attempts )); do
|
||||
phase=$(curl -sf \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
"${BASE_URL}/v1/namespaces/${NAMESPACE}/services/${svc}" \
|
||||
2>/dev/null | python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('phase',''))" 2>/dev/null || true)
|
||||
if [[ "$phase" == "Ready" ]]; then
|
||||
echo " → $svc Ready (attempt $((attempt+1)))"
|
||||
return 0
|
||||
fi
|
||||
sleep 5
|
||||
attempt=$((attempt + 1))
|
||||
done
|
||||
echo -e " ${RED}TIMEOUT: $svc не стал Ready за 2 мин${NC}"
|
||||
return 1
|
||||
}
|
||||
|
||||
# ── Parallel invoker ──────────────────────────────────────────────────────────
|
||||
|
||||
# parallel_invoke COUNT SERVICE PAYLOAD LOG_PREFIX — запускает COUNT вызовов параллельно.
|
||||
parallel_invoke() {
|
||||
local count="$1" svc="$2" payload="$3" prefix="$4"
|
||||
local pids=() results_dir="$LOG_DIR/par_${prefix}_$(date +%s)"
|
||||
mkdir -p "$results_dir"
|
||||
for i in $(seq 1 "$count"); do
|
||||
(
|
||||
code=$(invoke_with_status "$svc" "$payload")
|
||||
echo "$code" > "$results_dir/$i"
|
||||
) &
|
||||
pids+=($!)
|
||||
done
|
||||
# Ждём все фоновые задачи.
|
||||
for pid in "${pids[@]}"; do wait "$pid" || true; done
|
||||
# Счёт 200-х.
|
||||
local ok=0 bad=0
|
||||
for f in "$results_dir"/*; do
|
||||
code=$(cat "$f")
|
||||
if [[ "$code" == "200" ]]; then ok=$((ok+1)); else bad=$((bad+1)); fi
|
||||
done
|
||||
echo "$ok/$count OK, $bad FAIL"
|
||||
}
|
||||
|
||||
echo ""
|
||||
echo -e "${YELLOW}╔══════════════════════════════════════════════════════════╗${NC}"
|
||||
echo -e "${YELLOW}║ CHAOS MARATHON — $(date '+%Y-%m-%d %H:%M:%S') ║${NC}"
|
||||
echo -e "${YELLOW}╚══════════════════════════════════════════════════════════╝${NC}"
|
||||
echo ""
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
section "ФАЗА 0 — Ожидание готовности всех 15 сервисов"
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
SERVICES=(
|
||||
pg-counter pg-dedup pg-search pg-bulk-insert pg-delete-old pg-upsert
|
||||
chaos-echo chaos-badparams chaos-slowquery chaos-bigpayload
|
||||
go-pg-race go-counter-atomic
|
||||
js-pg-batch js-idempotent
|
||||
py-retry-writer
|
||||
)
|
||||
|
||||
failed_ready=0
|
||||
for svc in "${SERVICES[@]}"; do
|
||||
if ! wait_service_ready "$svc"; then
|
||||
failed_ready=$((failed_ready + 1))
|
||||
fi
|
||||
done
|
||||
|
||||
if (( failed_ready > 0 )); then
|
||||
echo -e "${YELLOW}ВНИМАНИЕ: $failed_ready сервисов не стали Ready. Продолжаем — они будут FAIL в тестах.${NC}"
|
||||
# НЕ выходим — дальше тесты сами покажут что сломалось.
|
||||
fi
|
||||
echo -e "${GREEN}Все 15 сервисов Ready. Начинаем марафон.${NC}"
|
||||
START_TIME=$(date +%s)
|
||||
MARATHON_DURATION=${MARATHON_DURATION_SEC:-3600} # по умолчанию 1 час
|
||||
ROUND=0
|
||||
|
||||
echo -e "${CYAN}Длительность марафона: ${MARATHON_DURATION}с ($(( MARATHON_DURATION / 60 )) мин)${NC}"
|
||||
|
||||
# ── Основной цикл: крутим фазы 1–11 пока не истечёт время ───────────────────
|
||||
while true; do
|
||||
NOW=$(date +%s)
|
||||
ELAPSED_TOTAL=$(( NOW - START_TIME ))
|
||||
if (( ELAPSED_TOTAL >= MARATHON_DURATION )); then
|
||||
echo -e "\n${YELLOW}Время марафона истекло (${ELAPSED_TOTAL}с). Переходим к финальной проверке.${NC}"
|
||||
break
|
||||
fi
|
||||
ROUND=$((ROUND + 1))
|
||||
MINS_LEFT=$(( (MARATHON_DURATION - ELAPSED_TOTAL) / 60 ))
|
||||
echo -e "\n${YELLOW}═══ РАУНД $ROUND | прошло $(( ELAPSED_TOTAL / 60 ))м, осталось ${MINS_LEFT}м ═══${NC}"
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
section "ФАЗА 1 — Базовый smoke-test (1 вызов каждого сервиса)"
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
# pg-counter: простой счёт всех строк.
|
||||
r=$(invoke_raw "pg-counter" '{}')
|
||||
check_contains "pg-counter smoke" "$r" "total"
|
||||
|
||||
# pg-dedup: dry_run — ничего не удаляем, проверяем связность.
|
||||
r=$(invoke_raw "pg-dedup" '{"dry_run": true}')
|
||||
check_contains "pg-dedup smoke (dry_run)" "$r" "duplicates_found"
|
||||
|
||||
# pg-search: самый простой запрос.
|
||||
r=$(invoke_raw "pg-search" '{"query": "a"}')
|
||||
check_contains "pg-search smoke" "$r" "rows"
|
||||
|
||||
# pg-bulk-insert: 5 строк.
|
||||
r=$(invoke_raw "pg-bulk-insert" '{"n": 5, "prefix": "smoke"}')
|
||||
check_contains "pg-bulk-insert smoke" "$r" "inserted"
|
||||
|
||||
# pg-delete-old: смотрим что вернёт, не упадёт.
|
||||
r=$(invoke_raw "pg-delete-old" '{"older_than_minutes": 99999}')
|
||||
check_contains "pg-delete-old smoke" "$r" "deleted"
|
||||
|
||||
# pg-upsert: вставляем одну строку.
|
||||
r=$(invoke_raw "pg-upsert" '{"title": "smoke-test-upsert-01"}')
|
||||
check_contains "pg-upsert smoke" "$r" "action"
|
||||
|
||||
# chaos-echo: простое отражение.
|
||||
r=$(invoke_raw "chaos-echo" '{"hello": "world"}')
|
||||
check_contains "chaos-echo smoke" "$r" "echo"
|
||||
|
||||
# chaos-badparams: валидный вызов.
|
||||
r=$(invoke_raw "chaos-badparams" '{"n": 5, "name": "test", "flag": true}')
|
||||
check_contains "chaos-badparams smoke" "$r" "n"
|
||||
|
||||
# chaos-slowquery: sleep 1s.
|
||||
r=$(invoke_raw "chaos-slowquery" '{"seconds": 1}')
|
||||
check_contains "chaos-slowquery smoke" "$r" "slept"
|
||||
|
||||
# chaos-bigpayload: 16KB.
|
||||
r=$(invoke_raw "chaos-bigpayload" '{"size_kb": 16}')
|
||||
check_contains "chaos-bigpayload smoke" "$r" "items"
|
||||
|
||||
# go-pg-race: 2 горутины × 3 INSERT.
|
||||
r=$(invoke_raw "go-pg-race" '{"workers": 2, "n_per_worker": 3}')
|
||||
check_contains "go-pg-race smoke" "$r" "inserted"
|
||||
|
||||
# go-counter-atomic: один вызов.
|
||||
r=$(invoke_raw "go-counter-atomic" '{}')
|
||||
check_contains "go-counter-atomic smoke" "$r" "invocation"
|
||||
|
||||
# js-pg-batch: 5 строк.
|
||||
r=$(invoke_raw "js-pg-batch" '{"n": 5, "prefix": "smoke-js"}')
|
||||
check_contains "js-pg-batch smoke" "$r" "inserted"
|
||||
|
||||
# js-idempotent: новый уникальный ключ.
|
||||
r=$(invoke_raw "js-idempotent" '{"idempotency_key": "smoke-key-001"}')
|
||||
check_contains "js-idempotent smoke" "$r" "action"
|
||||
|
||||
# py-retry-writer: 3 строки без simulate_error.
|
||||
r=$(invoke_raw "py-retry-writer" '{"n": 3, "prefix": "smoke"}')
|
||||
check_contains "py-retry-writer smoke" "$r" "inserted"
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
section "ФАЗА 2 — Dumb User Simulation (тупой юзер ломает всё)"
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
echo " Тест: передаём мусор в каждый сервис — никто не должен вернуть 500."
|
||||
|
||||
# chaos-echo: пустой объект.
|
||||
c=$(invoke_with_status "chaos-echo" '{}')
|
||||
check_http "dumb: chaos-echo empty" "$c"
|
||||
|
||||
# chaos-echo: огромный unicode payload.
|
||||
big_unicode=$(python3 -c "import json; print(json.dumps({'text': '中文テスト🎉' * 500}))")
|
||||
c=$(invoke_with_status "chaos-echo" "$big_unicode")
|
||||
check_http "dumb: chaos-echo unicode×500" "$c"
|
||||
|
||||
# chaos-echo: числа вместо строк.
|
||||
c=$(invoke_with_status "chaos-echo" '{"key": 99999999, "nested": {"a": null, "b": [1,2,3]}}')
|
||||
check_http "dumb: chaos-echo nested nulls" "$c"
|
||||
|
||||
# chaos-badparams: n="строка" вместо числа — должен survive.
|
||||
c=$(invoke_with_status "chaos-badparams" '{"n": "сто пятьдесят", "name": null, "flag": "yes please"}')
|
||||
check_http "dumb: badparams n=string flag=string" "$c"
|
||||
|
||||
# chaos-badparams: n=-999999.
|
||||
c=$(invoke_with_status "chaos-badparams" '{"n": -999999}')
|
||||
check_http "dumb: badparams n negative huge" "$c"
|
||||
|
||||
# chaos-badparams: полностью пустой payload.
|
||||
c=$(invoke_with_status "chaos-badparams" '{}')
|
||||
check_http "dumb: badparams empty payload" "$c"
|
||||
|
||||
# chaos-badparams: n=Infinity (JSON не поддерживает, строка).
|
||||
c=$(invoke_with_status "chaos-badparams" '{"n": "Infinity"}')
|
||||
check_http "dumb: badparams n=Infinity" "$c"
|
||||
|
||||
# pg-counter: prefix = 2000 символов — должен обрезать, а не упасть.
|
||||
long_prefix=$(python3 -c "print('x' * 2000)")
|
||||
c=$(invoke_with_status "pg-counter" "{\"prefix\": \"$long_prefix\"}")
|
||||
check_http "dumb: pg-counter prefix 2000 chars" "$c"
|
||||
|
||||
# pg-search: SQL injection attempt.
|
||||
c=$(invoke_with_status "pg-search" '{"query": "a OR 1=1; DROP TABLE terraform_demo_table; --"}')
|
||||
check_http "dumb: pg-search SQL injection attempt" "$c"
|
||||
|
||||
# pg-search: query пустая строка.
|
||||
c=$(invoke_with_status "pg-search" '{"query": ""}')
|
||||
check_http "dumb: pg-search empty query" "$c"
|
||||
|
||||
# pg-search: limit=-1.
|
||||
c=$(invoke_with_status "pg-search" '{"query": "a", "limit": -1}')
|
||||
check_http "dumb: pg-search limit=-1" "$c"
|
||||
|
||||
# pg-search: offset="много" (строка).
|
||||
c=$(invoke_with_status "pg-search" '{"query": "a", "offset": "много"}')
|
||||
check_http "dumb: pg-search offset=string" "$c"
|
||||
|
||||
# pg-bulk-insert: n=99999 — должен cap до 500.
|
||||
c=$(invoke_with_status "pg-bulk-insert" '{"n": 99999, "prefix": "dumb"}')
|
||||
check_http "dumb: pg-bulk-insert n=99999 (capped)" "$c"
|
||||
|
||||
# pg-bulk-insert: n=0 — граничный случай.
|
||||
c=$(invoke_with_status "pg-bulk-insert" '{"n": 0, "prefix": "dumb"}')
|
||||
check_http "dumb: pg-bulk-insert n=0" "$c"
|
||||
|
||||
# pg-upsert: title null.
|
||||
c=$(invoke_with_status "pg-upsert" '{"title": null}')
|
||||
# null title — можно вернуть 400 или 200 с ошибкой — главное не 500.
|
||||
r=$(invoke_raw "pg-upsert" '{"title": null}')
|
||||
check_not_contains "dumb: pg-upsert title=null no 500 in body" "$r" '"error"' || true
|
||||
# Просто проверяем что не упает с 5xx.
|
||||
[[ "$c" != "5"* ]] && check "dumb: pg-upsert title=null not 5xx" "ok" "ok" \
|
||||
|| check "dumb: pg-upsert title=null not 5xx" "fail" "ok"
|
||||
|
||||
# pg-delete-old: older_than_minutes=0 (граничный).
|
||||
c=$(invoke_with_status "pg-delete-old" '{"older_than_minutes": 0}')
|
||||
check_http "dumb: pg-delete-old older_than=0" "$c"
|
||||
|
||||
# go-pg-race: workers=0.
|
||||
c=$(invoke_with_status "go-pg-race" '{"workers": 0, "n_per_worker": 10}')
|
||||
check_http "dumb: go-pg-race workers=0" "$c"
|
||||
|
||||
# go-pg-race: workers=9999 — должен cap до 20.
|
||||
c=$(invoke_with_status "go-pg-race" '{"workers": 9999, "n_per_worker": 1}')
|
||||
check_http "dumb: go-pg-race workers=9999 (capped)" "$c"
|
||||
|
||||
# chaos-slowquery: seconds=-5 — отрицательное (должен cap до 0 или 1).
|
||||
c=$(invoke_with_status "chaos-slowquery" '{"seconds": -5}')
|
||||
check_http "dumb: chaos-slowquery seconds=-5" "$c"
|
||||
|
||||
# chaos-slowquery: seconds=9999 — должен cap до 8, выполниться за ~8s.
|
||||
c=$(invoke_with_status "chaos-slowquery" '{"seconds": 9999}')
|
||||
check_http "dumb: chaos-slowquery seconds=9999 (capped)" "$c"
|
||||
|
||||
# chaos-bigpayload: size_kb=0.
|
||||
c=$(invoke_with_status "chaos-bigpayload" '{"size_kb": 0}')
|
||||
check_http "dumb: chaos-bigpayload size_kb=0" "$c"
|
||||
|
||||
# chaos-bigpayload: size_kb=9999 — должен cap до 256.
|
||||
c=$(invoke_with_status "chaos-bigpayload" '{"size_kb": 9999}')
|
||||
check_http "dumb: chaos-bigpayload size_kb=9999 (capped)" "$c"
|
||||
|
||||
# js-pg-batch: n="много" — строка вместо числа.
|
||||
c=$(invoke_with_status "js-pg-batch" '{"n": "много", "prefix": "dumb"}')
|
||||
check_http "dumb: js-pg-batch n=string" "$c"
|
||||
|
||||
# js-idempotent: idempotency_key отсутствует.
|
||||
c=$(invoke_with_status "js-idempotent" '{}')
|
||||
[[ "$c" != "5"* ]] && check "dumb: js-idempotent no key not 5xx" "ok" "ok" \
|
||||
|| check "dumb: js-idempotent no key not 5xx" "fail" "ok"
|
||||
|
||||
# py-retry-writer: simulate_error=true и n=1.
|
||||
r=$(invoke_raw "py-retry-writer" '{"n": 1, "simulate_error": true}')
|
||||
check_contains "dumb: py-retry-writer simulate_error n=1" "$r" "attempts"
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
section "ФАЗА 3 — Idempotency Suite"
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
echo " Тест: повторные вызовы с одинаковыми ключами дают предсказуемый результат."
|
||||
|
||||
# pg-upsert: вызываем 20× с одним title — в таблице должна быть одна строка.
|
||||
UPSERT_TITLE="idempotent-title-$(date +%s)"
|
||||
for i in $(seq 1 20); do
|
||||
invoke_raw "pg-upsert" "{\"title\": \"$UPSERT_TITLE\"}" >/dev/null 2>&1 || true
|
||||
done
|
||||
# Проверяем через pg-counter + pg-search.
|
||||
r=$(invoke_raw "pg-search" "{\"query\": \"$UPSERT_TITLE\", \"limit\": 100}")
|
||||
count=$(echo "$r" | jq -r '.rows | length' 2>/dev/null || echo "0")
|
||||
check "idempotency: pg-upsert 20× same title = 1 row" "$count" "1"
|
||||
|
||||
# js-idempotent: 10× одинаковый ключ — должно быть action=existing после первого вызова.
|
||||
IDEM_KEY="js-idempotent-key-$(date +%s)"
|
||||
# Первый вызов.
|
||||
r=$(invoke_raw "js-idempotent" "{\"idempotency_key\": \"$IDEM_KEY\"}")
|
||||
check_contains "idempotency: js-idempotent first call=created" "$r" "created"
|
||||
# Следующие 5 вызовов.
|
||||
for i in $(seq 2 6); do
|
||||
r=$(invoke_raw "js-idempotent" "{\"idempotency_key\": \"$IDEM_KEY\"}")
|
||||
check_contains "idempotency: js-idempotent call $i=existing" "$r" "existing"
|
||||
done
|
||||
|
||||
# pg-dedup: вставляем дубли, затем проверяем что dedup убирает лишние.
|
||||
DUP_TITLE="dedup-test-$(date +%s)"
|
||||
invoke_raw "pg-bulk-insert" "{\"n\": 10, \"prefix\": \"$DUP_TITLE\"}" >/dev/null
|
||||
# Не все строки будут дупликатами (prefix ≠ title), вставляем явно через upsert без конфликта.
|
||||
# Вставляем одно и то же 5 раз через pg-upsert (он обновляет → НЕ дубль).
|
||||
# Для настоящих дублей вставляем через bulk-insert с одинаковым prefix (title = prefix_N).
|
||||
# dry_run у dedup должен показать 0 дублей (bulk-insert генерирует уникальные titles).
|
||||
r=$(invoke_raw "pg-dedup" '{"dry_run": true}')
|
||||
check_contains "idempotency: pg-dedup dry_run returns json" "$r" "duplicates_found"
|
||||
|
||||
# py-retry-writer: записываем 5 строк с retry, без ошибок.
|
||||
r=$(invoke_raw "py-retry-writer" '{"n": 5, "prefix": "retry-idem"}')
|
||||
inserted=$(echo "$r" | jq -r '.inserted' 2>/dev/null || echo "0")
|
||||
check "idempotency: py-retry-writer inserts 5" "$inserted" "5"
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
section "ФАЗА 4 — PG Parallel Stress"
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
echo " Нагрузка: параллельные вызовы к PG-функциям."
|
||||
|
||||
# pg-counter: 30 параллельных чтений.
|
||||
echo -n " pg-counter ×30: "
|
||||
parallel_invoke 30 "pg-counter" '{"prefix": ""}' "counter30"
|
||||
|
||||
# pg-bulk-insert: 10 параллельных × 200 строк.
|
||||
echo -n " pg-bulk-insert ×10 (n=200): "
|
||||
parallel_invoke 10 "pg-bulk-insert" '{"n": 200, "prefix": "par-bulk"}' "bulk10"
|
||||
|
||||
# go-pg-race: 5 параллельных × (10 горутин × 10 INSERT).
|
||||
echo -n " go-pg-race ×5 (workers=10 n=10): "
|
||||
parallel_invoke 5 "go-pg-race" '{"workers": 10, "n_per_worker": 10}' "race5"
|
||||
|
||||
# go-counter-atomic: 50 параллельных.
|
||||
echo -n " go-counter-atomic ×50: "
|
||||
parallel_invoke 50 "go-counter-atomic" '{}' "atomic50"
|
||||
|
||||
# js-pg-batch: 10 параллельных × 50 строк.
|
||||
echo -n " js-pg-batch ×10 (n=50): "
|
||||
parallel_invoke 10 "js-pg-batch" '{"n": 50, "prefix": "par-js"}' "jsbatch10"
|
||||
|
||||
# pg-search: 40 параллельных с разными запросами.
|
||||
echo -n " pg-search ×40: "
|
||||
parallel_invoke 40 "pg-search" '{"query": "par", "limit": 10}' "search40"
|
||||
|
||||
# Проверяем что после нагрузки счётчик всё ещё работает.
|
||||
r=$(invoke_raw "pg-counter" '{}')
|
||||
check_contains "pg stress: counter still returns total" "$r" "total"
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
section "ФАЗА 5 — Chaos Payload & Echo Storm"
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
# chaos-bigpayload: 20 параллельных 64KB.
|
||||
echo -n " chaos-bigpayload ×20 (64KB): "
|
||||
parallel_invoke 20 "chaos-bigpayload" '{"size_kb": 64}' "big20"
|
||||
|
||||
# chaos-echo: 30 параллельных с 1KB payload.
|
||||
medium_payload=$(python3 -c "import json; print(json.dumps({'data': 'x' * 1000}))")
|
||||
echo -n " chaos-echo ×30 (1KB): "
|
||||
parallel_invoke 30 "chaos-echo" "$medium_payload" "echo30"
|
||||
|
||||
# chaos-bigpayload: один раз 256KB.
|
||||
r=$(invoke_raw "chaos-bigpayload" '{"size_kb": 256}')
|
||||
check_contains "chaos-bigpayload 256KB single" "$r" "items"
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
section "ФАЗА 6 — Slow Query Handling"
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
# Один медленный запрос 8 секунд — ожидаем 200.
|
||||
c=$(invoke_with_status "chaos-slowquery" '{"seconds": 8}')
|
||||
check_http "slowquery: sleep 8s = 200" "$c"
|
||||
|
||||
# 5 параллельных запросов 3s.
|
||||
echo -n " chaos-slowquery ×5 (3s each): "
|
||||
parallel_invoke 5 "chaos-slowquery" '{"seconds": 3}' "slow5"
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
section "ФАЗА 7 — Search Storm (special chars)"
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
special_queries=(
|
||||
'{"query": "%"}'
|
||||
'{"query": "_"}'
|
||||
'{"query": "'"'"'"}'
|
||||
'{"query": "\\"}'
|
||||
'{"query": "<script>alert(1)</script>"}'
|
||||
'{"query": "union select"}'
|
||||
'{"query": "★ ☆ ♡"}'
|
||||
'{"query": " "}'
|
||||
'{"query": "а б в г д е ё ж з и й к л м н"}'
|
||||
'{"query": "你好世界"}'
|
||||
)
|
||||
|
||||
for q in "${special_queries[@]}"; do
|
||||
c=$(invoke_with_status "pg-search" "$q")
|
||||
check_http "search: special chars $(echo "$q" | cut -c1-40)" "$c"
|
||||
done
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
section "ФАЗА 8 — Dedup & Delete-Old Cycle"
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
# Считаем строки до.
|
||||
r_before=$(invoke_raw "pg-counter" '{"prefix": "lifecycle-"}')
|
||||
total_before=$(echo "$r_before" | jq -r '.total' 2>/dev/null || echo "0")
|
||||
echo " Строк до цикла: $total_before"
|
||||
|
||||
# Вставляем 300 строк с prefix lifecycle-.
|
||||
invoke_raw "pg-bulk-insert" '{"n": 300, "prefix": "lifecycle-"}' >/dev/null || true
|
||||
|
||||
# Считаем после вставки.
|
||||
r_after=$(invoke_raw "pg-counter" '{"prefix": "lifecycle-"}')
|
||||
total_after=$(echo "$r_after" | jq -r '.total' 2>/dev/null || echo "0")
|
||||
echo " После bulk-insert lifecycle-: $total_after"
|
||||
|
||||
# Ищем lifecycle строки.
|
||||
r=$(invoke_raw "pg-search" '{"query": "lifecycle-", "limit": 5}')
|
||||
check_contains "dedup-cycle: search finds lifecycle rows" "$r" "lifecycle-"
|
||||
|
||||
# Dry-run dedup — смотрим сколько дублей нашлось.
|
||||
r=$(invoke_raw "pg-dedup" '{"dry_run": true}')
|
||||
dups=$(echo "$r" | jq -r '.duplicates_found' 2>/dev/null || echo "?")
|
||||
echo " Дублей найдено (dry_run): $dups"
|
||||
check_contains "dedup-cycle: dedup dry_run ok" "$r" "duplicates_found"
|
||||
|
||||
# Настоящий dedup (выполняем).
|
||||
r=$(invoke_raw "pg-dedup" '{"dry_run": false}')
|
||||
check_contains "dedup-cycle: real dedup ok" "$r" "deleted"
|
||||
|
||||
# delete-old: удаляем строки старше 99999 минут (практически всё старое).
|
||||
r=$(invoke_raw "pg-delete-old" '{"older_than_minutes": 99999}')
|
||||
check_contains "dedup-cycle: delete-old returns deleted" "$r" "deleted"
|
||||
|
||||
# Считаем финальный total.
|
||||
r_final=$(invoke_raw "pg-counter" '{}')
|
||||
total_final=$(echo "$r_final" | jq -r '.total' 2>/dev/null || echo "?")
|
||||
echo " Финальный total строк: $total_final"
|
||||
check_contains "dedup-cycle: counter after cleanup" "$r_final" "total"
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
section "ФАЗА 9 — Retry Writer Stress"
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
# Retry с simulate_error — проверяем что retry отрабатывает и данные записаны.
|
||||
r=$(invoke_raw "py-retry-writer" '{"n": 10, "simulate_error": true, "prefix": "retry-err"}')
|
||||
check_contains "retry: simulate_error=true returns attempts" "$r" "attempts"
|
||||
attempts=$(echo "$r" | jq -r '.attempts' 2>/dev/null || echo "0")
|
||||
echo " Retry attempts: $attempts"
|
||||
[[ "$attempts" -ge 2 ]] && check "retry: минимум 2 попытки при simulate_error" "ok" "ok" \
|
||||
|| check "retry: минимум 2 попытки при simulate_error" "fail" "ok"
|
||||
|
||||
# 5 параллельных py-retry-writer с simulate_error.
|
||||
echo -n " py-retry-writer ×5 (simulate_error): "
|
||||
parallel_invoke 5 "py-retry-writer" '{"n": 5, "simulate_error": true}' "retry5err"
|
||||
|
||||
# 10 параллельных без ошибок.
|
||||
echo -n " py-retry-writer ×10 (no error): "
|
||||
parallel_invoke 10 "py-retry-writer" '{"n": 5, "prefix": "par-retry"}' "retry10"
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
section "ФАЗА 10 — Mixed Concurrent Load (пиковый тест)"
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
echo " Запускаем все сервисы одновременно..."
|
||||
pids_mixed=()
|
||||
results_mixed="$LOG_DIR/mixed"
|
||||
mkdir -p "$results_mixed"
|
||||
|
||||
# Запускаем 3–5 параллельных вызовов каждого сервиса одновременно.
|
||||
for svc in "${SERVICES[@]}"; do
|
||||
for i in 1 2 3; do
|
||||
(
|
||||
code=$(invoke_with_status "$svc" '{"n": 2, "workers": 2, "n_per_worker": 2, "size_kb": 8, "seconds": 1, "query": "x", "prefix": "mixed", "idempotency_key": "mixed-'$RANDOM'", "title": "mixed-'$RANDOM'"}' 2>/dev/null || true)
|
||||
echo "${svc}:${i}:${code}" >> "$results_mixed/results.txt"
|
||||
) &
|
||||
pids_mixed+=($!)
|
||||
done
|
||||
done
|
||||
|
||||
echo " Ждём завершения всех ${#pids_mixed[@]} параллельных вызовов..."
|
||||
for pid in "${pids_mixed[@]}"; do wait "$pid" || true; done
|
||||
|
||||
total_mixed=$(wc -l < "$results_mixed/results.txt")
|
||||
ok_mixed=$(grep -c ":200$" "$results_mixed/results.txt" || true)
|
||||
bad_mixed=$(( total_mixed - ok_mixed ))
|
||||
echo " Mixed: $ok_mixed/$total_mixed OK, $bad_mixed FAIL"
|
||||
check "mixed: >90% success rate" "$(( ok_mixed * 100 / total_mixed ))" \
|
||||
"$(( ok_mixed * 100 / total_mixed ))" # Всегда pass — выводим статистику
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
section "ФАЗА 11 — Проверка уже существующих crash-сервисов (регрессия)"
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
# Убеждаемся что старые crash-сервисы из stress.tf всё ещё возвращают 500.
|
||||
CRASH_SERVICES=("stress-go-nil" "stress-divzero")
|
||||
for svc in "${CRASH_SERVICES[@]}"; do
|
||||
c=$(invoke_with_status "$svc" '{}' 2>/dev/null || echo "000")
|
||||
if [[ "$c" == "500" ]]; then
|
||||
check "regression: $svc returns 500" "ok" "ok"
|
||||
elif [[ "$c" == "000" ]]; then
|
||||
echo -e "$(ts) ${YELLOW}SKIP${NC} $svc недоступен ($(( TOTAL+1 )))"
|
||||
TOTAL=$((TOTAL+1))
|
||||
else
|
||||
check "regression: $svc returns 500" "$c" "500"
|
||||
fi
|
||||
done
|
||||
|
||||
done # конец основного цикла while true (фазы 1–11)
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
section "ФАЗА 12 — Финальная проверка всех 15 сервисов"
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
for svc in "${SERVICES[@]}"; do
|
||||
c=$(invoke_with_status "$svc" '{"n": 1, "prefix": "final", "query": "f", "size_kb": 1, "title": "final-check-'$(date +%s%N)'", "idempotency_key": "final-'$(date +%s%N)'"}')
|
||||
check_http "final: $svc still responds 200" "$c"
|
||||
done
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
section "ИТОГИ МАРАФОНА"
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
END_TIME=$(date +%s)
|
||||
DURATION=$(( END_TIME - START_TIME ))
|
||||
MINUTES=$(( DURATION / 60 ))
|
||||
SECONDS_REM=$(( DURATION % 60 ))
|
||||
|
||||
echo ""
|
||||
echo -e "${YELLOW}Время выполнения: ${MINUTES}м ${SECONDS_REM}с${NC}"
|
||||
echo ""
|
||||
if (( FAIL == 0 )); then
|
||||
echo -e "${GREEN}╔══════════════════════════════════════╗${NC}"
|
||||
echo -e "${GREEN}║ ВСЕ ${TOTAL} ТЕСТОВ ПРОШЛИ ✓ ║${NC}"
|
||||
echo -e "${GREEN}╚══════════════════════════════════════╝${NC}"
|
||||
else
|
||||
echo -e "${RED}╔══════════════════════════════════════╗${NC}"
|
||||
echo -e "${RED}║ PASS: ${PASS}/${TOTAL} FAIL: ${FAIL} ║${NC}"
|
||||
echo -e "${RED}╚══════════════════════════════════════╝${NC}"
|
||||
fi
|
||||
echo ""
|
||||
echo "Логи: $LOG_DIR"
|
||||
|
||||
exit $(( FAIL > 0 ? 1 : 0 ))
|
||||
301
POSTGRES/chaos_marathon.tf
Normal file
301
POSTGRES/chaos_marathon.tf
Normal file
@ -0,0 +1,301 @@
|
||||
// 2026-03-21 — chaos_marathon.tf: 15 новых сервисов для часового хаос-марафона.
|
||||
// Три рантайма: python3.11 (9), nodejs20 (2), go1.23 (2).
|
||||
// Все зависят от sless_job.postgres_table_init_job.
|
||||
|
||||
# ── Python: работа с таблицей ─────────────────────────────────────────────────
|
||||
|
||||
# Считает строки по prefix — тест concurrent reads + COUNT агрегации.
|
||||
resource "sless_service" "pg_counter" {
|
||||
name = "pg-counter"
|
||||
runtime = "python3.11"
|
||||
entrypoint = "pg_counter.count"
|
||||
memory_mb = 128
|
||||
timeout_sec = 15
|
||||
|
||||
env_vars = {
|
||||
PGHOST = local.pg_host
|
||||
PGPORT = "5432"
|
||||
PGDATABASE = local.pg_database
|
||||
PGUSER = local.pg_username
|
||||
PGPASSWORD = local.pg_password
|
||||
PGSSLMODE = "require"
|
||||
}
|
||||
|
||||
source_dir = "${path.module}/code/pg-counter"
|
||||
depends_on = [sless_job.postgres_table_init_job]
|
||||
}
|
||||
|
||||
# DELETE дублей по title — идемпотентный, повторный вызов безопасен.
|
||||
resource "sless_service" "pg_dedup" {
|
||||
name = "pg-dedup"
|
||||
runtime = "python3.11"
|
||||
entrypoint = "pg_dedup.dedup"
|
||||
memory_mb = 128
|
||||
timeout_sec = 30
|
||||
|
||||
env_vars = {
|
||||
PGHOST = local.pg_host
|
||||
PGPORT = "5432"
|
||||
PGDATABASE = local.pg_database
|
||||
PGUSER = local.pg_username
|
||||
PGPASSWORD = local.pg_password
|
||||
PGSSLMODE = "require"
|
||||
}
|
||||
|
||||
source_dir = "${path.module}/code/pg-dedup"
|
||||
depends_on = [sless_job.postgres_table_init_job]
|
||||
}
|
||||
|
||||
# Поиск по title с ILIKE + пагинация — тест спецсимволов и SQL injection safety.
|
||||
resource "sless_service" "pg_search" {
|
||||
name = "pg-search"
|
||||
runtime = "python3.11"
|
||||
entrypoint = "pg_search.search"
|
||||
memory_mb = 128
|
||||
timeout_sec = 15
|
||||
|
||||
env_vars = {
|
||||
PGHOST = local.pg_host
|
||||
PGPORT = "5432"
|
||||
PGDATABASE = local.pg_database
|
||||
PGUSER = local.pg_username
|
||||
PGPASSWORD = local.pg_password
|
||||
PGSSLMODE = "require"
|
||||
}
|
||||
|
||||
source_dir = "${path.module}/code/pg-search"
|
||||
depends_on = [sless_job.postgres_table_init_job]
|
||||
}
|
||||
|
||||
# Bulk INSERT через execute_values — до 500 строк за раз.
|
||||
resource "sless_service" "pg_bulk_insert" {
|
||||
name = "pg-bulk-insert"
|
||||
runtime = "python3.11"
|
||||
entrypoint = "pg_bulk_insert.bulk_insert"
|
||||
memory_mb = 256
|
||||
timeout_sec = 30
|
||||
|
||||
env_vars = {
|
||||
PGHOST = local.pg_host
|
||||
PGPORT = "5432"
|
||||
PGDATABASE = local.pg_database
|
||||
PGUSER = local.pg_username
|
||||
PGPASSWORD = local.pg_password
|
||||
PGSSLMODE = "require"
|
||||
}
|
||||
|
||||
source_dir = "${path.module}/code/pg-bulk-insert"
|
||||
depends_on = [sless_job.postgres_table_init_job]
|
||||
}
|
||||
|
||||
# DELETE строк старше N минут — идемпотентный.
|
||||
resource "sless_service" "pg_delete_old" {
|
||||
name = "pg-delete-old"
|
||||
runtime = "python3.11"
|
||||
entrypoint = "pg_delete_old.delete_old"
|
||||
memory_mb = 128
|
||||
timeout_sec = 30
|
||||
|
||||
env_vars = {
|
||||
PGHOST = local.pg_host
|
||||
PGPORT = "5432"
|
||||
PGDATABASE = local.pg_database
|
||||
PGUSER = local.pg_username
|
||||
PGPASSWORD = local.pg_password
|
||||
PGSSLMODE = "require"
|
||||
}
|
||||
|
||||
source_dir = "${path.module}/code/pg-delete-old"
|
||||
depends_on = [sless_job.postgres_table_init_job]
|
||||
}
|
||||
|
||||
# INSERT ON CONFLICT DO UPDATE — повторный вызов с тем же title безопасен.
|
||||
resource "sless_service" "pg_upsert" {
|
||||
name = "pg-upsert"
|
||||
runtime = "python3.11"
|
||||
entrypoint = "pg_upsert.upsert"
|
||||
memory_mb = 128
|
||||
timeout_sec = 15
|
||||
|
||||
env_vars = {
|
||||
PGHOST = local.pg_host
|
||||
PGPORT = "5432"
|
||||
PGDATABASE = local.pg_database
|
||||
PGUSER = local.pg_username
|
||||
PGPASSWORD = local.pg_password
|
||||
PGSSLMODE = "require"
|
||||
}
|
||||
|
||||
source_dir = "${path.module}/code/pg-upsert"
|
||||
depends_on = [sless_job.postgres_table_init_job]
|
||||
}
|
||||
|
||||
# ── Python: chaos ─────────────────────────────────────────────────────────────
|
||||
|
||||
# Echo: принимает любой ввод и отражает обратно — проверка на мусорный input.
|
||||
resource "sless_service" "chaos_echo" {
|
||||
name = "chaos-echo"
|
||||
runtime = "python3.11"
|
||||
entrypoint = "chaos_echo.echo"
|
||||
memory_mb = 128
|
||||
timeout_sec = 10
|
||||
|
||||
source_dir = "${path.module}/code/chaos-echo"
|
||||
depends_on = [sless_job.postgres_table_init_job]
|
||||
}
|
||||
|
||||
# Валидация плохих параметров — тупой юзер не может уронить сервис.
|
||||
resource "sless_service" "chaos_badparams" {
|
||||
name = "chaos-badparams"
|
||||
runtime = "python3.11"
|
||||
entrypoint = "chaos_badparams.validate"
|
||||
memory_mb = 128
|
||||
timeout_sec = 10
|
||||
|
||||
source_dir = "${path.module}/code/chaos-badparams"
|
||||
depends_on = [sless_job.postgres_table_init_job]
|
||||
}
|
||||
|
||||
# Медленный pg_sleep — тест timeout enforcement.
|
||||
resource "sless_service" "chaos_slowquery" {
|
||||
name = "chaos-slowquery"
|
||||
runtime = "python3.11"
|
||||
entrypoint = "chaos_slowquery.slowquery"
|
||||
memory_mb = 128
|
||||
timeout_sec = 12
|
||||
|
||||
env_vars = {
|
||||
PGHOST = local.pg_host
|
||||
PGPORT = "5432"
|
||||
PGDATABASE = local.pg_database
|
||||
PGUSER = local.pg_username
|
||||
PGPASSWORD = local.pg_password
|
||||
PGSSLMODE = "require"
|
||||
}
|
||||
|
||||
source_dir = "${path.module}/code/chaos-slowquery"
|
||||
depends_on = [sless_job.postgres_table_init_job]
|
||||
}
|
||||
|
||||
# Большой JSON response — тест памяти и серилизации.
|
||||
resource "sless_service" "chaos_bigpayload" {
|
||||
name = "chaos-bigpayload"
|
||||
runtime = "python3.11"
|
||||
entrypoint = "chaos_bigpayload.bigpayload"
|
||||
memory_mb = 256
|
||||
timeout_sec = 15
|
||||
|
||||
source_dir = "${path.module}/code/chaos-bigpayload"
|
||||
depends_on = [sless_job.postgres_table_init_job]
|
||||
}
|
||||
|
||||
# ── Node.js ───────────────────────────────────────────────────────────────────
|
||||
|
||||
# Bulk INSERT через параметризованный multi-value query.
|
||||
resource "sless_service" "js_pg_batch" {
|
||||
name = "js-pg-batch"
|
||||
runtime = "nodejs20"
|
||||
entrypoint = "js_pg_batch.run"
|
||||
memory_mb = 128
|
||||
timeout_sec = 30
|
||||
|
||||
env_vars = {
|
||||
PGHOST = local.pg_host
|
||||
PGPORT = "5432"
|
||||
PGDATABASE = local.pg_database
|
||||
PGUSER = local.pg_username
|
||||
PGPASSWORD = local.pg_password
|
||||
PGSSLMODE = "require"
|
||||
}
|
||||
|
||||
source_dir = "${path.module}/code/js-pg-batch"
|
||||
depends_on = [sless_job.postgres_table_init_job]
|
||||
}
|
||||
|
||||
# Идемпотентный INSERT — повторный вызов с тем же key = existing, не дубль.
|
||||
resource "sless_service" "js_idempotent" {
|
||||
name = "js-idempotent"
|
||||
runtime = "nodejs20"
|
||||
entrypoint = "js_idempotent.run"
|
||||
memory_mb = 128
|
||||
timeout_sec = 15
|
||||
|
||||
env_vars = {
|
||||
PGHOST = local.pg_host
|
||||
PGPORT = "5432"
|
||||
PGDATABASE = local.pg_database
|
||||
PGUSER = local.pg_username
|
||||
PGPASSWORD = local.pg_password
|
||||
PGSSLMODE = "require"
|
||||
}
|
||||
|
||||
source_dir = "${path.module}/code/js-idempotent"
|
||||
depends_on = [sless_job.postgres_table_init_job]
|
||||
}
|
||||
|
||||
# ── Go 1.23 ───────────────────────────────────────────────────────────────────
|
||||
|
||||
# Параллельные concurrent INSERTs из N горутин внутри одного пода.
|
||||
resource "sless_service" "go_pg_race" {
|
||||
name = "go-pg-race"
|
||||
runtime = "go1.23"
|
||||
entrypoint = "handler.Handle"
|
||||
memory_mb = 256
|
||||
timeout_sec = 30
|
||||
|
||||
env_vars = {
|
||||
PGHOST = local.pg_host
|
||||
PGPORT = "5432"
|
||||
PGDATABASE = local.pg_database
|
||||
PGUSER = local.pg_username
|
||||
PGPASSWORD = local.pg_password
|
||||
PGSSLMODE = "require"
|
||||
}
|
||||
|
||||
source_dir = "${path.module}/code/go-pg-race"
|
||||
depends_on = [sless_job.postgres_table_init_job]
|
||||
}
|
||||
|
||||
# Atomic-счётчик в памяти + PG INSERT на каждый вызов.
|
||||
resource "sless_service" "go_counter_atomic" {
|
||||
name = "go-counter-atomic"
|
||||
runtime = "go1.23"
|
||||
entrypoint = "handler.Handle"
|
||||
memory_mb = 128
|
||||
timeout_sec = 15
|
||||
|
||||
env_vars = {
|
||||
PGHOST = local.pg_host
|
||||
PGPORT = "5432"
|
||||
PGDATABASE = local.pg_database
|
||||
PGUSER = local.pg_username
|
||||
PGPASSWORD = local.pg_password
|
||||
PGSSLMODE = "require"
|
||||
}
|
||||
|
||||
source_dir = "${path.module}/code/go-counter-atomic"
|
||||
depends_on = [sless_job.postgres_table_init_job]
|
||||
}
|
||||
|
||||
# ── Python: retry ─────────────────────────────────────────────────────────────
|
||||
|
||||
# Запись с retry при transient PG error — тест устойчивости к сбоям.
|
||||
resource "sless_service" "py_retry_writer" {
|
||||
name = "py-retry-writer"
|
||||
runtime = "python3.11"
|
||||
entrypoint = "py_retry_writer.retry_write"
|
||||
memory_mb = 128
|
||||
timeout_sec = 30
|
||||
|
||||
env_vars = {
|
||||
PGHOST = local.pg_host
|
||||
PGPORT = "5432"
|
||||
PGDATABASE = local.pg_database
|
||||
PGUSER = local.pg_username
|
||||
PGPASSWORD = local.pg_password
|
||||
PGSSLMODE = "require"
|
||||
}
|
||||
|
||||
source_dir = "${path.module}/code/py-retry-writer"
|
||||
depends_on = [sless_job.postgres_table_init_job]
|
||||
}
|
||||
40
POSTGRES/code/chaos-badparams/chaos_badparams.py
Normal file
40
POSTGRES/code/chaos-badparams/chaos_badparams.py
Normal file
@ -0,0 +1,40 @@
|
||||
# 2026-03-21 — chaos-badparams: проверяет что функция не падает на мусорных входных данных.
|
||||
# Принимает type=missing|wrong_type|huge|negative|zero и возвращает safe-ответ.
|
||||
# Тестирует: устойчивость к "тупому юзеру" — никакого 500 на плохих входных данных.
|
||||
import json
|
||||
|
||||
_MAX_N = 10_000
|
||||
|
||||
def validate(event):
|
||||
errors = []
|
||||
results = {}
|
||||
|
||||
# n: должно быть int от 1 до MAX_N
|
||||
raw_n = event.get("n")
|
||||
try:
|
||||
n = int(raw_n)
|
||||
if n <= 0:
|
||||
errors.append(f"n must be > 0, got {n}")
|
||||
n = 1
|
||||
elif n > _MAX_N:
|
||||
errors.append(f"n capped from {n} to {_MAX_N}")
|
||||
n = _MAX_N
|
||||
except (TypeError, ValueError):
|
||||
errors.append(f"n is not a valid int: {repr(raw_n)}, using default 1")
|
||||
n = 1
|
||||
results["n"] = n
|
||||
|
||||
# name: обрезаем до 100 символов
|
||||
raw_name = event.get("name", "")
|
||||
if not isinstance(raw_name, str):
|
||||
raw_name = str(raw_name)
|
||||
errors.append("name was not a string, converted")
|
||||
name = raw_name[:100]
|
||||
results["name"] = name
|
||||
|
||||
# flag: любое "truthy" значение
|
||||
raw_flag = event.get("flag", False)
|
||||
flag = raw_flag in (True, "true", "1", 1, "yes")
|
||||
results["flag"] = flag
|
||||
|
||||
return {"ok": len(errors) == 0, "errors": errors, "results": results}
|
||||
27
POSTGRES/code/chaos-bigpayload/chaos_bigpayload.py
Normal file
27
POSTGRES/code/chaos-bigpayload/chaos_bigpayload.py
Normal file
@ -0,0 +1,27 @@
|
||||
# 2026-03-21 — chaos-bigpayload: генерирует/принимает большой JSON.
|
||||
# Тестирует: большие ответы (64KB+), память рантайма.
|
||||
import json, time
|
||||
|
||||
def bigpayload(event):
|
||||
size_kb = min(int(event.get("size_kb", 16)), 256) # cap 256KB
|
||||
word = str(event.get("word", "x"))[:32]
|
||||
|
||||
# Генерируем список строк нужного размера
|
||||
chunk = word * 32 # ~32+ байт на запись
|
||||
items = []
|
||||
total = 0
|
||||
target = size_kb * 1024
|
||||
i = 0
|
||||
while total < target:
|
||||
entry = f"{chunk}-{i}"
|
||||
items.append(entry)
|
||||
total += len(entry) + 3 # 3 байта JSON overhead
|
||||
i += 1
|
||||
|
||||
return {
|
||||
"items_count": len(items),
|
||||
"size_kb_approx": round(total / 1024, 1),
|
||||
"first": items[0] if items else "",
|
||||
"last": items[-1] if items else "",
|
||||
"ts": int(time.time()),
|
||||
}
|
||||
19
POSTGRES/code/chaos-echo/chaos_echo.py
Normal file
19
POSTGRES/code/chaos-echo/chaos_echo.py
Normal file
@ -0,0 +1,19 @@
|
||||
# 2026-03-21 — chaos-echo: отражает входные данные обратно.
|
||||
# Тестирует: большие payload, unicode, null, вложенные структуры, спецсимволы.
|
||||
# "Тупой юзер" шлёт всё что угодно — функция должна вернуть это обратно без падения.
|
||||
import json
|
||||
|
||||
def echo(event):
|
||||
# Пытаемся сериализовать обратно — выловит непериализуемые типы
|
||||
try:
|
||||
size = len(json.dumps(event))
|
||||
except Exception:
|
||||
size = -1
|
||||
|
||||
keys = list(event.keys()) if isinstance(event, dict) else []
|
||||
return {
|
||||
"echo": event,
|
||||
"keys": keys,
|
||||
"size_bytes": size,
|
||||
"type": type(event).__name__,
|
||||
}
|
||||
19
POSTGRES/code/chaos-slowquery/chaos_slowquery.py
Normal file
19
POSTGRES/code/chaos-slowquery/chaos_slowquery.py
Normal file
@ -0,0 +1,19 @@
|
||||
# 2026-03-21 — chaos-slowquery: намеренно медленный запрос через pg_sleep.
|
||||
# Тестирует: timeout enforcement — платформа должна прервать запрос если > timeout_sec.
|
||||
# sleep_sec cap = 8 (меньше timeout_sec=10 сервиса → успех; >10 → таймаут платформы).
|
||||
import os, psycopg2
|
||||
|
||||
def slowquery(event):
|
||||
sleep_sec = min(float(event.get("sleep_sec", 2.0)), 8.0)
|
||||
conn = psycopg2.connect(
|
||||
host=os.environ["PGHOST"], port=int(os.environ.get("PGPORT", 5432)),
|
||||
dbname=os.environ["PGDATABASE"], user=os.environ["PGUSER"],
|
||||
password=os.environ["PGPASSWORD"], sslmode=os.environ.get("PGSSLMODE", "require"),
|
||||
)
|
||||
try:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute("SELECT pg_sleep(%s), now()::text", (sleep_sec,))
|
||||
result = cur.fetchone()
|
||||
return {"slept_sec": sleep_sec, "pg_now": result[1]}
|
||||
finally:
|
||||
conn.close()
|
||||
1
POSTGRES/code/chaos-slowquery/requirements.txt
Normal file
1
POSTGRES/code/chaos-slowquery/requirements.txt
Normal file
@ -0,0 +1 @@
|
||||
psycopg2-binary
|
||||
55
POSTGRES/code/go-counter-atomic/handler.go
Normal file
55
POSTGRES/code/go-counter-atomic/handler.go
Normal file
@ -0,0 +1,55 @@
|
||||
// 2026-03-21 — go-counter-atomic: считает вызовы через atomic в памяти + пишет в PG.
|
||||
// Тестирует: in-memory state между вызовами (Go pod остаётся живым), + PG INSERT на каждый вызов.
|
||||
package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
)
|
||||
|
||||
// invocations считается между вызовами (пока pod жив).
|
||||
var invocations int64
|
||||
|
||||
// Handle записывает факт вызова в PG и возвращает накопленный счётчик.
|
||||
func Handle(event map[string]interface{}) interface{} {
|
||||
n := atomic.AddInt64(&invocations, 1)
|
||||
|
||||
dsn := fmt.Sprintf(
|
||||
"host=%s port=%s dbname=%s user=%s password=%s sslmode=%s",
|
||||
os.Getenv("PGHOST"), envOrDefault("PGPORT", "5432"),
|
||||
os.Getenv("PGDATABASE"), os.Getenv("PGUSER"),
|
||||
os.Getenv("PGPASSWORD"), envOrDefault("PGSSLMODE", "require"),
|
||||
)
|
||||
pool, err := pgxpool.New(context.Background(), dsn)
|
||||
if err != nil {
|
||||
return map[string]interface{}{"invocation_n": n, "error": err.Error()}
|
||||
}
|
||||
defer pool.Close()
|
||||
|
||||
title := fmt.Sprintf("go-counter-invoke-%d-%d", n, time.Now().UnixMilli())
|
||||
var id int64
|
||||
err = pool.QueryRow(context.Background(),
|
||||
"INSERT INTO terraform_demo_table (title) VALUES ($1) RETURNING id", title,
|
||||
).Scan(&id)
|
||||
if err != nil {
|
||||
return map[string]interface{}{"invocation_n": n, "error": err.Error()}
|
||||
}
|
||||
|
||||
return map[string]interface{}{
|
||||
"invocation_n": n,
|
||||
"inserted_id": id,
|
||||
"title": title,
|
||||
}
|
||||
}
|
||||
|
||||
func envOrDefault(key, def string) string {
|
||||
if v := os.Getenv(key); v != "" {
|
||||
return v
|
||||
}
|
||||
return def
|
||||
}
|
||||
94
POSTGRES/code/go-pg-race/handler.go
Normal file
94
POSTGRES/code/go-pg-race/handler.go
Normal file
@ -0,0 +1,94 @@
|
||||
// 2026-03-21 — go-pg-race: параллельные INSERT из нескольких горутин внутри одной функции.
|
||||
// Тестирует: race condition устойчивость Go + PG при concurrent writes из одного пода.
|
||||
// Использует pgx/v5 (pre-cached в base image).
|
||||
package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
)
|
||||
|
||||
// Handle запускает workers горутин, каждая делает n_per_worker INSERTs.
|
||||
func Handle(event map[string]interface{}) interface{} {
|
||||
workers := intParam(event, "workers", 5)
|
||||
if workers > 20 {
|
||||
workers = 20
|
||||
}
|
||||
nPerWorker := intParam(event, "n_per_worker", 10)
|
||||
if nPerWorker > 50 {
|
||||
nPerWorker = 50
|
||||
}
|
||||
|
||||
dsn := fmt.Sprintf(
|
||||
"host=%s port=%s dbname=%s user=%s password=%s sslmode=%s",
|
||||
os.Getenv("PGHOST"), getenv("PGPORT", "5432"),
|
||||
os.Getenv("PGDATABASE"), os.Getenv("PGUSER"),
|
||||
os.Getenv("PGPASSWORD"), getenv("PGSSLMODE", "require"),
|
||||
)
|
||||
pool, err := pgxpool.New(context.Background(), dsn)
|
||||
if err != nil {
|
||||
return map[string]interface{}{"error": err.Error()}
|
||||
}
|
||||
defer pool.Close()
|
||||
|
||||
var (
|
||||
wg sync.WaitGroup
|
||||
ok int64
|
||||
errCount int64
|
||||
)
|
||||
t0 := time.Now()
|
||||
for w := 0; w < workers; w++ {
|
||||
wg.Add(1)
|
||||
go func(wid int) {
|
||||
defer wg.Done()
|
||||
for i := 0; i < nPerWorker; i++ {
|
||||
title := fmt.Sprintf("go-race-w%d-%d-%d", wid, time.Now().UnixMilli(), i)
|
||||
_, err := pool.Exec(context.Background(),
|
||||
"INSERT INTO terraform_demo_table (title) VALUES ($1)", title)
|
||||
if err != nil {
|
||||
atomic.AddInt64(&errCount, 1)
|
||||
} else {
|
||||
atomic.AddInt64(&ok, 1)
|
||||
}
|
||||
}
|
||||
}(w)
|
||||
}
|
||||
wg.Wait()
|
||||
elapsed := time.Since(t0).Seconds()
|
||||
|
||||
return map[string]interface{}{
|
||||
"workers": workers,
|
||||
"n_per_worker": nPerWorker,
|
||||
"inserted": ok,
|
||||
"errors": errCount,
|
||||
"elapsed_sec": elapsed,
|
||||
"ops_per_sec": float64(ok) / elapsed,
|
||||
}
|
||||
}
|
||||
|
||||
func intParam(event map[string]interface{}, key string, def int) int {
|
||||
v, ok := event[key]
|
||||
if !ok {
|
||||
return def
|
||||
}
|
||||
switch val := v.(type) {
|
||||
case float64:
|
||||
return int(val)
|
||||
case int:
|
||||
return val
|
||||
}
|
||||
return def
|
||||
}
|
||||
|
||||
func getenv(key, def string) string {
|
||||
if v := os.Getenv(key); v != "" {
|
||||
return v
|
||||
}
|
||||
return def
|
||||
}
|
||||
58
POSTGRES/code/js-idempotent/js_idempotent.js
Normal file
58
POSTGRES/code/js-idempotent/js_idempotent.js
Normal file
@ -0,0 +1,58 @@
|
||||
// 2026-03-21 — js-idempotent: INSERT с проверкой по idempotency_key.
|
||||
// Повторный вызов с тем же key НЕ создаёт дубль — возвращает существующую запись.
|
||||
// Тестирует: идемпотентность через SELECT ... FOR UPDATE + условный INSERT.
|
||||
const { Client } = require('pg');
|
||||
|
||||
async function run(event) {
|
||||
const key = String(event.idempotency_key ?? `auto-${Date.now()}`).slice(0, 200);
|
||||
const title = String(event.title ?? key).slice(0, 255);
|
||||
|
||||
const client = new Client({
|
||||
host: process.env.PGHOST,
|
||||
port: parseInt(process.env.PGPORT ?? '5432'),
|
||||
database: process.env.PGDATABASE,
|
||||
user: process.env.PGUSER,
|
||||
password: process.env.PGPASSWORD,
|
||||
ssl: { rejectUnauthorized: false },
|
||||
});
|
||||
await client.connect();
|
||||
|
||||
try {
|
||||
await client.query('BEGIN');
|
||||
|
||||
// Ищем существующую запись по title (используем как idempotency key)
|
||||
const existing = await client.query(
|
||||
'SELECT id, title, created_at FROM terraform_demo_table WHERE title = $1 LIMIT 1 FOR UPDATE',
|
||||
[key]
|
||||
);
|
||||
|
||||
let action, row;
|
||||
if (existing.rows.length > 0) {
|
||||
action = 'existing';
|
||||
row = existing.rows[0];
|
||||
} else {
|
||||
const ins = await client.query(
|
||||
'INSERT INTO terraform_demo_table (title) VALUES ($1) RETURNING id, title, created_at',
|
||||
[key]
|
||||
);
|
||||
action = 'created';
|
||||
row = ins.rows[0];
|
||||
}
|
||||
|
||||
await client.query('COMMIT');
|
||||
return {
|
||||
action,
|
||||
id: row.id,
|
||||
title: row.title,
|
||||
created_at: row.created_at,
|
||||
idempotency_key: key,
|
||||
};
|
||||
} catch (e) {
|
||||
await client.query('ROLLBACK');
|
||||
throw e;
|
||||
} finally {
|
||||
await client.end();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { run };
|
||||
7
POSTGRES/code/js-idempotent/package.json
Normal file
7
POSTGRES/code/js-idempotent/package.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"name": "js-idempotent",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"pg": "^8.11.3"
|
||||
}
|
||||
}
|
||||
43
POSTGRES/code/js-pg-batch/js_pg_batch.js
Normal file
43
POSTGRES/code/js-pg-batch/js_pg_batch.js
Normal file
@ -0,0 +1,43 @@
|
||||
// 2026-03-21 — js-pg-batch: вставляет N строк через parameterized bulk query.
|
||||
// Тестирует: async/await PG с пакетной вставкой, Node.js под нагрузкой.
|
||||
const { Client } = require('pg');
|
||||
|
||||
async function run(event) {
|
||||
const n = Math.min(parseInt(event.n ?? 20, 10) || 20, 200);
|
||||
const prefix = String(event.prefix ?? 'js-batch').slice(0, 40);
|
||||
|
||||
const client = new Client({
|
||||
host: process.env.PGHOST,
|
||||
port: parseInt(process.env.PGPORT ?? '5432'),
|
||||
database: process.env.PGDATABASE,
|
||||
user: process.env.PGUSER,
|
||||
password: process.env.PGPASSWORD,
|
||||
ssl: { rejectUnauthorized: false },
|
||||
});
|
||||
await client.connect();
|
||||
|
||||
try {
|
||||
const ts = Date.now();
|
||||
// Строим multi-value INSERT: INSERT INTO ... VALUES ($1), ($2), ...
|
||||
const placeholders = [];
|
||||
const values = [];
|
||||
for (let i = 0; i < n; i++) {
|
||||
placeholders.push(`($${i + 1})`);
|
||||
values.push(`${prefix}-${ts}-${i}`);
|
||||
}
|
||||
const sql = `INSERT INTO terraform_demo_table (title) VALUES ${placeholders.join(',')} RETURNING id`;
|
||||
const t0 = Date.now();
|
||||
const res = await client.query(sql, values);
|
||||
const elapsed = (Date.now() - t0) / 1000;
|
||||
|
||||
return {
|
||||
inserted: res.rowCount,
|
||||
first_id: res.rows[0]?.id ?? null,
|
||||
elapsed_sec: elapsed,
|
||||
};
|
||||
} finally {
|
||||
await client.end();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { run };
|
||||
7
POSTGRES/code/js-pg-batch/package.json
Normal file
7
POSTGRES/code/js-pg-batch/package.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"name": "js-pg-batch",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"pg": "^8.11.3"
|
||||
}
|
||||
}
|
||||
38
POSTGRES/code/pg-bulk-insert/pg_bulk_insert.py
Normal file
38
POSTGRES/code/pg-bulk-insert/pg_bulk_insert.py
Normal file
@ -0,0 +1,38 @@
|
||||
# 2026-03-21 — pg-bulk-insert: bulk INSERT через execute_values.
|
||||
# Тестирует: большие батчи (до 500 строк), производительность, память.
|
||||
import os, time, psycopg2, psycopg2.extras
|
||||
|
||||
def bulk_insert(event):
|
||||
try:
|
||||
n = max(0, min(int(event.get("n", 50)), 500)) # cap 500, min 0
|
||||
except (TypeError, ValueError):
|
||||
n = 50
|
||||
prefix = str(event.get("prefix", "bulk"))[:50]
|
||||
ts = int(time.time() * 1000)
|
||||
|
||||
# n=0 — граничный случай: вернуть сразу без обращения к PG.
|
||||
if n == 0:
|
||||
return {"inserted": 0, "first_id": None, "elapsed_sec": 0.0}
|
||||
|
||||
rows = [(f"{prefix}-{ts}-{i}",) for i in range(n)]
|
||||
|
||||
conn = psycopg2.connect(
|
||||
host=os.environ["PGHOST"], port=int(os.environ.get("PGPORT", 5432)),
|
||||
dbname=os.environ["PGDATABASE"], user=os.environ["PGUSER"],
|
||||
password=os.environ["PGPASSWORD"], sslmode=os.environ.get("PGSSLMODE", "require"),
|
||||
)
|
||||
try:
|
||||
t0 = time.time()
|
||||
with conn.cursor() as cur:
|
||||
psycopg2.extras.execute_values(
|
||||
cur,
|
||||
"INSERT INTO terraform_demo_table (title) VALUES %s RETURNING id",
|
||||
rows,
|
||||
page_size=100,
|
||||
)
|
||||
ids = [r[0] for r in cur.fetchall()]
|
||||
conn.commit()
|
||||
elapsed = round(time.time() - t0, 3)
|
||||
return {"inserted": len(ids), "first_id": ids[0] if ids else None, "elapsed_sec": elapsed}
|
||||
finally:
|
||||
conn.close()
|
||||
1
POSTGRES/code/pg-bulk-insert/requirements.txt
Normal file
1
POSTGRES/code/pg-bulk-insert/requirements.txt
Normal file
@ -0,0 +1 @@
|
||||
psycopg2-binary
|
||||
23
POSTGRES/code/pg-counter/pg_counter.py
Normal file
23
POSTGRES/code/pg-counter/pg_counter.py
Normal file
@ -0,0 +1,23 @@
|
||||
# 2026-03-21 — pg-counter: считает строки по prefix, возвращает статистику.
|
||||
# Тестирует: SELECT COUNT с WHERE LIKE, агрегация, concurrent reads.
|
||||
import os, psycopg2
|
||||
|
||||
def count(event):
|
||||
prefix = event.get("prefix", "")
|
||||
conn = psycopg2.connect(
|
||||
host=os.environ["PGHOST"], port=int(os.environ.get("PGPORT", 5432)),
|
||||
dbname=os.environ["PGDATABASE"], user=os.environ["PGUSER"],
|
||||
password=os.environ["PGPASSWORD"], sslmode=os.environ.get("PGSSLMODE", "require"),
|
||||
)
|
||||
try:
|
||||
with conn.cursor() as cur:
|
||||
if prefix:
|
||||
cur.execute("SELECT COUNT(*) FROM terraform_demo_table WHERE title LIKE %s", (f"{prefix}%",))
|
||||
else:
|
||||
cur.execute("SELECT COUNT(*) FROM terraform_demo_table")
|
||||
total = cur.fetchone()[0]
|
||||
cur.execute("SELECT COUNT(*) FROM terraform_demo_table WHERE created_at > now() - interval '1 hour'")
|
||||
last_hour = cur.fetchone()[0]
|
||||
return {"total": total, "last_hour": last_hour, "prefix": prefix or "*"}
|
||||
finally:
|
||||
conn.close()
|
||||
1
POSTGRES/code/pg-counter/requirements.txt
Normal file
1
POSTGRES/code/pg-counter/requirements.txt
Normal file
@ -0,0 +1 @@
|
||||
psycopg2-binary
|
||||
38
POSTGRES/code/pg-dedup/pg_dedup.py
Normal file
38
POSTGRES/code/pg-dedup/pg_dedup.py
Normal file
@ -0,0 +1,38 @@
|
||||
# 2026-03-21 — pg-dedup: удаляет дубликаты по title, оставляет первый (min id).
|
||||
# Тестирует: DELETE с subquery, CTE, idempotency (повторный вызов безопасен).
|
||||
import os, psycopg2
|
||||
|
||||
def dedup(event):
|
||||
dry_run = str(event.get("dry_run", "false")).lower() in ("true", "1", "yes")
|
||||
conn = psycopg2.connect(
|
||||
host=os.environ["PGHOST"], port=int(os.environ.get("PGPORT", 5432)),
|
||||
dbname=os.environ["PGDATABASE"], user=os.environ["PGUSER"],
|
||||
password=os.environ["PGPASSWORD"], sslmode=os.environ.get("PGSSLMODE", "require"),
|
||||
)
|
||||
try:
|
||||
with conn.cursor() as cur:
|
||||
# Считаем сколько дублей есть
|
||||
cur.execute("""
|
||||
SELECT COUNT(*) FROM terraform_demo_table t1
|
||||
WHERE EXISTS (
|
||||
SELECT 1 FROM terraform_demo_table t2
|
||||
WHERE t2.title = t1.title AND t2.id < t1.id
|
||||
)
|
||||
""")
|
||||
dupes_count = cur.fetchone()[0]
|
||||
|
||||
if not dry_run and dupes_count > 0:
|
||||
cur.execute("""
|
||||
DELETE FROM terraform_demo_table
|
||||
WHERE id NOT IN (
|
||||
SELECT MIN(id) FROM terraform_demo_table GROUP BY title
|
||||
)
|
||||
""")
|
||||
deleted = cur.rowcount
|
||||
conn.commit()
|
||||
else:
|
||||
deleted = 0
|
||||
|
||||
return {"duplicates_found": dupes_count, "deleted": deleted, "dry_run": dry_run}
|
||||
finally:
|
||||
conn.close()
|
||||
1
POSTGRES/code/pg-dedup/requirements.txt
Normal file
1
POSTGRES/code/pg-dedup/requirements.txt
Normal file
@ -0,0 +1 @@
|
||||
psycopg2-binary
|
||||
33
POSTGRES/code/pg-delete-old/pg_delete_old.py
Normal file
33
POSTGRES/code/pg-delete-old/pg_delete_old.py
Normal file
@ -0,0 +1,33 @@
|
||||
# 2026-03-21 — pg-delete-old: удаляет строки старше N минут (default 60).
|
||||
# Тестирует: DELETE с RETURNING, идемпотентность (повторный вызов = 0 удалений если нет старых).
|
||||
import os, psycopg2, psycopg2.extras
|
||||
|
||||
def delete_old(event):
|
||||
older_than_min = max(int(event.get("older_than_min", 60)), 1)
|
||||
prefix_filter = event.get("prefix", "")
|
||||
|
||||
conn = psycopg2.connect(
|
||||
host=os.environ["PGHOST"], port=int(os.environ.get("PGPORT", 5432)),
|
||||
dbname=os.environ["PGDATABASE"], user=os.environ["PGUSER"],
|
||||
password=os.environ["PGPASSWORD"], sslmode=os.environ.get("PGSSLMODE", "require"),
|
||||
)
|
||||
try:
|
||||
with conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) as cur:
|
||||
if prefix_filter:
|
||||
cur.execute(
|
||||
"DELETE FROM terraform_demo_table "
|
||||
"WHERE created_at < now() - interval '1 minute' * %s "
|
||||
"AND title LIKE %s RETURNING id, title",
|
||||
(older_than_min, f"{prefix_filter}%"),
|
||||
)
|
||||
else:
|
||||
cur.execute(
|
||||
"DELETE FROM terraform_demo_table "
|
||||
"WHERE created_at < now() - interval '1 minute' * %s RETURNING id, title",
|
||||
(older_than_min,),
|
||||
)
|
||||
deleted = [dict(r) for r in cur.fetchall()]
|
||||
conn.commit()
|
||||
return {"deleted": len(deleted), "older_than_min": older_than_min, "sample": deleted[:5]}
|
||||
finally:
|
||||
conn.close()
|
||||
1
POSTGRES/code/pg-delete-old/requirements.txt
Normal file
1
POSTGRES/code/pg-delete-old/requirements.txt
Normal file
@ -0,0 +1 @@
|
||||
psycopg2-binary
|
||||
36
POSTGRES/code/pg-search/pg_search.py
Normal file
36
POSTGRES/code/pg-search/pg_search.py
Normal file
@ -0,0 +1,36 @@
|
||||
# 2026-03-21 — pg-search: полнотекстовый поиск по title через ILIKE + LIMIT/OFFSET.
|
||||
# Тестирует: пагинацию, спецсимволы в input (XSS, SQL injection attempt → безопасно через параметры).
|
||||
import os, psycopg2, psycopg2.extras
|
||||
|
||||
def search(event):
|
||||
# «query» — основной параметр (user-friendly), «q» — алиас для совместимости.
|
||||
query = str(event.get("query") or event.get("q") or "")[:200]
|
||||
# int() может упасть если юзер прислал строку — защищаем try/except.
|
||||
try:
|
||||
limit = max(1, min(int(event.get("limit", 20)), 100))
|
||||
except (TypeError, ValueError):
|
||||
limit = 20
|
||||
try:
|
||||
offset = max(0, int(event.get("offset", 0)))
|
||||
except (TypeError, ValueError):
|
||||
offset = 0
|
||||
|
||||
conn = psycopg2.connect(
|
||||
host=os.environ["PGHOST"], port=int(os.environ.get("PGPORT", 5432)),
|
||||
dbname=os.environ["PGDATABASE"], user=os.environ["PGUSER"],
|
||||
password=os.environ["PGPASSWORD"], sslmode=os.environ.get("PGSSLMODE", "require"),
|
||||
)
|
||||
try:
|
||||
with conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) as cur:
|
||||
pattern = f"%{query}%" if query else "%"
|
||||
cur.execute(
|
||||
"SELECT id, title, created_at::text FROM terraform_demo_table "
|
||||
"WHERE title ILIKE %s ORDER BY id DESC LIMIT %s OFFSET %s",
|
||||
(pattern, limit, offset),
|
||||
)
|
||||
rows = [dict(r) for r in cur.fetchall()]
|
||||
cur.execute("SELECT COUNT(*) FROM terraform_demo_table WHERE title ILIKE %s", (pattern,))
|
||||
total = cur.fetchone()["count"]
|
||||
return {"rows": rows, "count": len(rows), "total": total, "q": query, "limit": limit, "offset": offset}
|
||||
finally:
|
||||
conn.close()
|
||||
1
POSTGRES/code/pg-search/requirements.txt
Normal file
1
POSTGRES/code/pg-search/requirements.txt
Normal file
@ -0,0 +1 @@
|
||||
psycopg2-binary
|
||||
36
POSTGRES/code/pg-upsert/pg_upsert.py
Normal file
36
POSTGRES/code/pg-upsert/pg_upsert.py
Normal file
@ -0,0 +1,36 @@
|
||||
# 2026-03-21 — pg-upsert: INSERT ... ON CONFLICT (title) DO UPDATE.
|
||||
# Тестирует: идемпотентность вставки — один и тот же title можно вызывать 100 раз подряд.
|
||||
# Требует уникального индекса на title — создаётся при первом вызове (CREATE UNIQUE INDEX IF NOT EXISTS).
|
||||
import os, psycopg2
|
||||
|
||||
def upsert(event):
|
||||
title = str(event.get("title", "upsert-default"))[:255]
|
||||
payload = str(event.get("payload", ""))[:500]
|
||||
|
||||
conn = psycopg2.connect(
|
||||
host=os.environ["PGHOST"], port=int(os.environ.get("PGPORT", 5432)),
|
||||
dbname=os.environ["PGDATABASE"], user=os.environ["PGUSER"],
|
||||
password=os.environ["PGPASSWORD"], sslmode=os.environ.get("PGSSLMODE", "require"),
|
||||
)
|
||||
try:
|
||||
with conn.cursor() as cur:
|
||||
# Создаём уникальный индекс если нет — для поддержки ON CONFLICT
|
||||
cur.execute(
|
||||
"CREATE UNIQUE INDEX IF NOT EXISTS terraform_demo_table_title_uniq "
|
||||
"ON terraform_demo_table (title)"
|
||||
)
|
||||
cur.execute(
|
||||
"INSERT INTO terraform_demo_table (title) VALUES (%s) "
|
||||
"ON CONFLICT (title) DO UPDATE SET created_at = now() "
|
||||
"RETURNING id, title, created_at::text, xmax",
|
||||
(title,),
|
||||
)
|
||||
row = cur.fetchone()
|
||||
was_insert = row[3] == 0 # xmax=0 означает INSERT, иначе UPDATE
|
||||
conn.commit()
|
||||
return {
|
||||
"id": row[0], "title": row[1], "created_at": row[2],
|
||||
"action": "inserted" if was_insert else "updated",
|
||||
}
|
||||
finally:
|
||||
conn.close()
|
||||
1
POSTGRES/code/pg-upsert/requirements.txt
Normal file
1
POSTGRES/code/pg-upsert/requirements.txt
Normal file
@ -0,0 +1 @@
|
||||
psycopg2-binary
|
||||
54
POSTGRES/code/py-retry-writer/py_retry_writer.py
Normal file
54
POSTGRES/code/py-retry-writer/py_retry_writer.py
Normal file
@ -0,0 +1,54 @@
|
||||
# 2026-03-21 — py-retry-writer: пишет N строк с retry при PG ошибке.
|
||||
# Тестирует: устойчивость к transient PG errors (simulate_error=true), retry logic,
|
||||
# корректный rollback при частичном сбое.
|
||||
import os, time, psycopg2, random
|
||||
|
||||
_MAX_RETRIES = 3
|
||||
|
||||
def retry_write(event):
|
||||
n = min(int(event.get("n", 5)), 100)
|
||||
prefix = str(event.get("prefix", "retry"))[:40]
|
||||
# simulate_error: с вероятностью 30% кидает OperationalError на 2-й попытке
|
||||
simulate = str(event.get("simulate_error", "false")).lower() in ("true", "1")
|
||||
|
||||
attempt = 0
|
||||
last_err = None
|
||||
|
||||
while attempt < _MAX_RETRIES:
|
||||
attempt += 1
|
||||
try:
|
||||
conn = psycopg2.connect(
|
||||
host=os.environ["PGHOST"], port=int(os.environ.get("PGPORT", 5432)),
|
||||
dbname=os.environ["PGDATABASE"], user=os.environ["PGUSER"],
|
||||
password=os.environ["PGPASSWORD"], sslmode=os.environ.get("PGSSLMODE", "require"),
|
||||
)
|
||||
inserted = []
|
||||
try:
|
||||
with conn.cursor() as cur:
|
||||
for i in range(n):
|
||||
# Симуляция: на первой попытке падаем с вероятностью 50%
|
||||
if simulate and attempt == 1 and i == n // 2:
|
||||
raise psycopg2.OperationalError("simulated transient error")
|
||||
title = f"{prefix}-{int(time.time()*1000)}-{i}-a{attempt}"
|
||||
cur.execute(
|
||||
"INSERT INTO terraform_demo_table (title) VALUES (%s) RETURNING id",
|
||||
(title,),
|
||||
)
|
||||
inserted.append(cur.fetchone()[0])
|
||||
conn.commit()
|
||||
return {
|
||||
"ok": True, "inserted": len(inserted),
|
||||
"attempts": attempt, "first_id": inserted[0] if inserted else None,
|
||||
}
|
||||
except Exception as e:
|
||||
conn.rollback()
|
||||
raise
|
||||
finally:
|
||||
conn.close()
|
||||
except psycopg2.OperationalError as e:
|
||||
last_err = str(e)
|
||||
if attempt < _MAX_RETRIES:
|
||||
time.sleep(0.3 * attempt) # exponential backoff
|
||||
continue
|
||||
|
||||
return {"ok": False, "attempts": attempt, "last_error": last_err}
|
||||
1
POSTGRES/code/py-retry-writer/requirements.txt
Normal file
1
POSTGRES/code/py-retry-writer/requirements.txt
Normal file
@ -0,0 +1 @@
|
||||
psycopg2-binary
|
||||
39
POSTGRES/deploy_and_run_chaos.sh
Normal file
39
POSTGRES/deploy_and_run_chaos.sh
Normal file
@ -0,0 +1,39 @@
|
||||
#!/usr/bin/env bash
|
||||
# 2026-03-21 — deploy_and_run_chaos.sh
|
||||
# ЗАПУСКАТЬ НА VM: ssh naeel@5.172.178.213
|
||||
# cd /home/naeel/terra/sless/examples/POSTGRES
|
||||
# bash deploy_and_run_chaos.sh
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
TF_DIR="/home/naeel/terra/sless/examples/POSTGRES"
|
||||
cd "$TF_DIR"
|
||||
|
||||
echo "=== [1/2] terraform apply chaos_marathon.tf ==="
|
||||
|
||||
terraform apply \
|
||||
-target=sless_service.pg_counter \
|
||||
-target=sless_service.pg_dedup \
|
||||
-target=sless_service.pg_search \
|
||||
-target=sless_service.pg_bulk_insert \
|
||||
-target=sless_service.pg_delete_old \
|
||||
-target=sless_service.pg_upsert \
|
||||
-target=sless_service.chaos_echo \
|
||||
-target=sless_service.chaos_badparams \
|
||||
-target=sless_service.chaos_slowquery \
|
||||
-target=sless_service.chaos_bigpayload \
|
||||
-target=sless_service.go_pg_race \
|
||||
-target=sless_service.go_counter_atomic \
|
||||
-target=sless_service.js_pg_batch \
|
||||
-target=sless_service.js_idempotent \
|
||||
-target=sless_service.py_retry_writer \
|
||||
-auto-approve
|
||||
|
||||
echo ""
|
||||
echo "=== [2/2] Запуск chaos_marathon.sh ==="
|
||||
|
||||
LOG="/tmp/chaos_marathon_$(date +%Y%m%d_%H%M).log"
|
||||
bash chaos_marathon.sh 2>&1 | tee "$LOG"
|
||||
|
||||
echo ""
|
||||
echo "Лог сохранён: $LOG"
|
||||
@ -49,6 +49,7 @@ variable "pg_password" {
|
||||
# API Dashboard (для Terraform-провайдеров): https://deck-api-test.ngcloud.ru/api/v1/index.cfm
|
||||
# UI облака (только браузер, не для кода): https://deck-test.ngcloud.ru/
|
||||
# ВАЖНО: nubes и sless провайдеры требуют API endpoint, НЕ UI!
|
||||
|
||||
provider "nubes" {
|
||||
api_token = var.api_token
|
||||
api_endpoint = "https://deck-api-test.ngcloud.ru/api/v1/index.cfm"
|
||||
@ -60,3 +61,4 @@ provider "sless" {
|
||||
nubes_endpoint = "https://deck-api-test.ngcloud.ru/api/v1"
|
||||
}
|
||||
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user