fix(runtime): Go panic→500 (recover), Python exception→500+threading+backlog

- go1.23 v0.1.2: defer recover() в HTTP handler — panic no longer closes connection → HTTP 500
- python3.11 v0.1.5: try/except в do_GET/_handle_with_body/do_HEAD → 500 вместо EOF
- python3.11 v0.1.5: ThreadingHTTPServer — concurrent requests (был single-thread)
- python3.11 v0.1.6: _HighBacklogHTTPServer(request_queue_size=128) — listen(128) вместо listen(5)
- context.go: go1.23 v0.1.1→v0.1.2, python3.11 v0.1.4→v0.1.6
- operator: v0.1.45 → v0.1.47 (два деплоя подряд с новыми runtime-версиями)
- examples/POSTGRES: stress.tf (10 сервисов), full_test.sh (48 тестов, 4 фазы)
- examples/POSTGRES: README.md, очистка от старых файлов (luceUNDnode.tf, funcs_list.py)

Результат: full_test.sh 48/48 PASS
- Фаза 3 PG-стресс: 40/40 parallel writer OK, 30/30 js-async OK, pgstorm 14k ops 0 err
- Фаза 4 краш-шторм: 75/75 × HTTP 500 (паники не роняют платформу)
This commit is contained in:
Naeel
2026-03-21 08:42:03 +03:00
parent 0592f57fb6
commit 778cbc8b32
11 changed files with 814 additions and 271 deletions
+3 -3
View File
@@ -1,9 +1,9 @@
# Изменено: 2026-03-11 # Изменено: 2026-03-21
# Деплой sless оператора в кластер. # Деплой sless оператора в кластер.
# Состав: # Состав:
# - ConfigMap: не-секретные env vars (S3_ENDPOINT, REGISTRY_HOST и т.д.) # - ConfigMap: не-секретные env vars (S3_ENDPOINT, REGISTRY_HOST и т.д.)
# - Secret: секретные данные (S3 keys, postgres DSN, API token, Harbor pass) # - Secret: секретные данные (S3 keys, postgres DSN, API token, Harbor pass)
# - Deployment: оператор naeel/sless-operator:v0.1.44 в namespace sless # - Deployment: оператор naeel/sless-operator:v0.1.46 в namespace sless
# - Service: ClusterIP :9090 (REST API) # - Service: ClusterIP :9090 (REST API)
# - Ingress: sless.kube5s.ru → :9090 (внешний доступ с TLS) # - Ingress: sless.kube5s.ru → :9090 (внешний доступ с TLS)
# #
@@ -74,7 +74,7 @@ spec:
containers: containers:
- name: operator - name: operator
# При обновлении версии оператора — менять тег здесь (не latest!) # При обновлении версии оператора — менять тег здесь (не latest!)
image: pearlharbor.registryk8s.services.ngcloud.ru/naeel/sless-operator:v0.1.45 image: pearlharbor.registryk8s.services.ngcloud.ru/naeel/sless-operator:v0.1.47
# Always — чтобы всегда тянуть по точному тегу (не кешировать старый) # Always — чтобы всегда тянуть по точному тегу (не кешировать старый)
imagePullPolicy: Always imagePullPolicy: Always
ports: ports:
+100
View File
@@ -0,0 +1,100 @@
# POSTGRES — Пример: Serverless-функции с Managed PostgreSQL
Демонстрирует интеграцию sless (serverless functions) с управляемым PostgreSQL (nubes_postgres).
## Что делает этот пример
1. **Создаёт Managed PostgreSQL** через Terraform (nubes_postgres + nubes_postgres_user + nubes_postgres_database)
2. **Инициализирует БД**: одноразовый `sless_job` создаёт таблицу `terraform_demo_table`
3. **Запускает 3 HTTP-сервиса**:
- `pg-info` (Node.js 20) — версия PostgreSQL-сервера + количество строк в таблице
- `pg-table-reader` (Python 3.11) — чтение всех строк из таблицы
- `pg-table-writer` (Python 3.11) — добавление новой строки
## Структура файлов
```
POSTGRES/
├── main.tf # terraform + провайдеры (sless, nubes_cloud)
├── postgres.tf # Managed PostgreSQL: DB, пользователь, locals с credentials
├── resources.tf # Namespace и сетевые ресурсы
├── functions.tf # sless_job (init) + 3 x sless_service
├── terraform.tfvars # Переменные: realm, s3_uid, token
├── stress_test.sh # Стресс-тест функций (не трогает PG lifecycle)
├── stress_destroy_apply.sh.disabled # ОТКЛЮЧЁН — стресс-тест PG lifecycle
├── code/
│ ├── sql-runner/ # Python: одноразовое выполнение SQL (CREATE TABLE)
│ ├── pg-info/ # Node.js: версия PG + строки
│ ├── table-rw/ # Python: list_rows + add_row
│ ├── pg-stats/ # Python: расширенная статистика PG
│ ├── funcs-list/ # Утилита: листинг функций
│ └── stress-*/ # Функции для стресс-тестирования
└── scripts/ # Вспомогательные скрипты
```
## Как запустить
### Предварительные требования
- Terraform >= 1.3
- Токен sless: `SLESS_TOKEN` (или в `terraform.tfvars`)
- Токен nubes_cloud: `NUBES_TOKEN`
- Доступ к realm (например, `ffd1f598c169b0ae`)
### Запуск
```bash
# 1. Инициализация
terraform init
# 2. Проверка плана
terraform plan
# 3. Применение (создаст PG + сервисы, запустит init job)
terraform apply
```
> Первый `apply` может занять 10–15 минут: создание PG-инстанса + kaniko-сборка образов.
### Переменные (`terraform.tfvars`)
```hcl
realm = "ffd1f598c169b0ae" # Реалм (namespace в sless)
s3_uid = "s01234" # S3 bucket для nubes_postgres бэкапов
sless_token = "..." # Bearer-токен для sless API
nubes_token = "..." # Bearer-токен для nubes_cloud API
```
### Вывод после apply
```
Outputs:
table_reader_url = "https://sless.kube5s.ru/v1/namespaces/.../services/pg-table-reader/invoke"
table_writer_url = "https://sless.kube5s.ru/v1/namespaces/.../services/pg-table-writer/invoke"
```
### Вызов функций
```bash
# Информация о PG (Node.js)
curl https://.../services/pg-info/invoke
# Список строк таблицы (Python)
curl https://.../services/pg-table-reader/invoke
# Добавить строку (Python)
curl -X POST https://.../services/pg-table-writer/invoke \
-H "Content-Type: application/json" \
-d '{"title": "Hello from sless!"}'
```
## Стресс-тест
`stress_test.sh` — нагружает функции HTTP-запросами. Запускать после `terraform apply`:
```bash
./stress_test.sh
```
> `stress_destroy_apply.sh.disabled` — ранний тест PG lifecycle (destroy+apply цикл).
> **Отключён** из-за проблем с удалением postgres_user в определённых сценариях.
@@ -1,4 +1,3 @@
package handler
// 2026-03-19 // 2026-03-19
// handler.go — быстрая Go функция: факториал + числа Фибоначчи. // handler.go — быстрая Go функция: факториал + числа Фибоначчи.
// Проверяет Go runtime под лёгкой нагрузкой и корректность JSON-ответа. // Проверяет Go runtime под лёгкой нагрузкой и корректность JSON-ответа.
+437
View File
@@ -0,0 +1,437 @@
#!/bin/bash
# 2026-03-21 — full_test.sh: комплексный тест всех sless-ресурсов.
#
# Фазы:
# 1. CRUD — проверяем наличие всех сервисов через API
# 2. Функциональные — корректность ответов, правильные значения
# 3. PG-стресс — параллельные write/read, pgstorm (Go), js-async storm
# 4. Краш-шторм — параллельные паники, проверяем что платформа жива после
#
# Запуск: bash full_test.sh
# Зависимости: curl, python3, terraform (для CRUD destroy/create)
#
# Среда: namespace sless-ffd1f598c169b0ae, токен в ~/terra/sless/test.token
set -uo pipefail
TOKEN=$(cat /home/naeel/terra/sless/test.token)
NS="sless-ffd1f598c169b0ae"
BASE="https://sless.kube5s.ru/fn/$NS"
API="https://sless.kube5s.ru/v1/namespaces/$NS"
GREEN='\033[0;32m'
RED='\033[0;31m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
NC='\033[0m'
PASS=0
FAIL=0
pass() { echo -e " ${GREEN}[PASS]${NC} $1"; ((PASS++)); }
fail() { echo -e " ${RED}[FAIL]${NC} $1"; ((FAIL++)); }
section() { echo -e "\n${YELLOW}━━━ $1 ━━━${NC}"; }
info() { echo -e " ${CYAN}[INFO]${NC} $1"; }
# Вызвать URL и вернуть JSON (не проверяя код)
call() {
local url="$1" body="${2:-}" extra_headers="${3:-}"
local args=(-s -m 90 -H "Authorization: Bearer $TOKEN")
[[ -n "$body" ]] && args+=(-H "Content-Type: application/json" -d "$body")
[[ -n "$extra_headers" ]] && args+=(-H "$extra_headers")
curl "${args[@]}" "$url"
}
# Проверить HTTP-код (только код, без тела)
check_http() {
local label="$1" url="$2" method="${3:-GET}" body="${4:-}" expect="${5:-200}"
local args=(-s -o /dev/null -w "%{http_code}" -m 90 -H "Authorization: Bearer $TOKEN")
[[ -n "$body" ]] && args+=(-H "Content-Type: application/json" -d "$body")
[[ "$method" != "GET" ]] && args+=(-X "$method")
local code
code=$(curl "${args[@]}" "$url")
if [[ "$code" == "$expect" ]]; then
pass "$label → HTTP $code"
else
fail "$label → HTTP $code (ожидали $expect)"
fi
}
# Вызвать функцию, проверить поле JSON == expected
check_field() {
local label="$1" url="$2" body="$3" field="$4" expected="$5"
local resp
resp=$(call "$url" "$body")
local actual
actual=$(echo "$resp" | python3 -c "
import sys, json
try:
d = json.load(sys.stdin)
v = d.get('$field', '__MISSING__')
print(str(v))
except Exception as e:
print('PARSE_ERROR: ' + str(e))
" 2>/dev/null)
if [[ "$actual" == "$expected" ]]; then
pass "$label"
else
fail "$label → got '$actual' (ожидали '$expected') | resp: $(echo "$resp" | head -c 200)"
fi
}
# Вызвать функцию, проверить что поле JSON > 0 (числовое)
check_field_gt0() {
local label="$1" url="$2" body="$3" field="$4"
local resp
resp=$(call "$url" "$body")
local actual
actual=$(echo "$resp" | python3 -c "
import sys, json
try:
d = json.load(sys.stdin)
v = d.get('$field', 0)
print(1 if float(str(v)) > 0 else 0)
except:
print(0)
" 2>/dev/null)
if [[ "$actual" == "1" ]]; then
pass "$label"
else
fail "$label → resp: $(echo "$resp" | head -c 200)"
fi
}
# ═══════════════════════════════════════════════════════════════
section "ФАЗА 1: CRUD — проверяем что все сервисы существуют"
# ═══════════════════════════════════════════════════════════════
ALL_SERVICES=(
pg-info pg-table-reader pg-table-writer
stress-go-fast stress-go-nil stress-go-pgstorm
stress-js-async stress-js-badenv
stress-slow stress-bigloop stress-divzero stress-writer pg-stats
)
for svc in "${ALL_SERVICES[@]}"; do
code=$(curl -s -o /dev/null -w "%{http_code}" -m 10 \
-H "Authorization: Bearer $TOKEN" "$API/services/$svc")
if [[ "$code" == "200" ]]; then
pass "API GET /services/$svc → 200"
else
fail "API GET /services/$svc$code"
fi
done
info "Проверяем несуществующий сервис → 404"
check_http "GET /services/THIS-SERVICE-DOES-NOT-EXIST → 404" \
"$API/services/this-service-does-not-exist" "GET" "" "404"
info "Проверяем jobs"
code=$(curl -s -o /dev/null -w "%{http_code}" -m 10 \
-H "Authorization: Bearer $TOKEN" "$API/jobs/pg-create-table-job-main-v13")
if [[ "$code" == "200" ]]; then
pass "API GET /jobs/pg-create-table-job-main-v13 → 200"
else
fail "API GET /jobs/pg-create-table-job-main-v13 → $code"
fi
# ═══════════════════════════════════════════════════════════════
section "ФАЗА 2: Функциональные тесты (корректность ответов)"
# ═══════════════════════════════════════════════════════════════
info "── Go 1.23 ──"
# stress-go-fast: factorial(10) = 3628800
check_field "go-fast runtime=go1.23" \
"$BASE/stress-go-fast" '{"n":10}' "runtime" "go1.23"
check_field "go-fast factorial(10)=3628800" \
"$BASE/stress-go-fast" '{"n":10}' "factorial" "3628800"
check_field "go-fast fib(10)=55" \
"$BASE/stress-go-fast" '{"n":10}' "fib" "55"
# n>20 обрезается до 20 — проверяем граничный случай
check_field "go-fast n=21 обрезается до 20: fib(20)=6765" \
"$BASE/stress-go-fast" '{"n":21}' "fib" "6765"
# stress-go-nil crash=false → crashed:false
check_field "go-nil crash=false → crashed=False" \
"$BASE/stress-go-nil" '{"crash":false}' "crashed" "False"
# stress-go-nil crash=true → 500
check_http "go-nil crash=true → HTTP 500" \
"$BASE/stress-go-nil" "POST" '{"crash":true}' "500"
# stress-go-nil default (no body) → 500 (по умолчанию crash=true)
check_http "go-nil без параметров → HTTP 500" \
"$BASE/stress-go-nil" "GET" "" "500"
info "── Node.js 20 ──"
# stress-js-async: чтение PG, возвращает pg_version
check_field "js-async runtime=nodejs20" \
"$BASE/stress-js-async" "" "runtime" "nodejs20"
check_field_gt0 "js-async total_rows > 0" \
"$BASE/stress-js-async" "" "total_rows"
# stress-js-badenv crash=false → ok
check_field "js-badenv crash=false → runtime=nodejs20" \
"$BASE/stress-js-badenv" '{"crash":false}' "runtime" "nodejs20"
# stress-js-badenv crash=true → 500
check_http "js-badenv crash=true → HTTP 500" \
"$BASE/stress-js-badenv" "POST" '{"crash":true}' "500"
info "── Python 3.11 ──"
# stress-slow
check_field "slow: slept_sec=3" \
"$BASE/stress-slow" '{"sleep":3}' "slept_sec" "3"
check_field "slow: version=v1" \
"$BASE/stress-slow" '{"sleep":1}' "version" "v1"
# stress-bigloop: sum(i*i for i in range(10)) = 285
check_field "bigloop n=10 sum_of_squares=285" \
"$BASE/stress-bigloop" '{"n":10}' "sum_of_squares" "285"
# range(100): 0+1+4+...+9801 = sum(i^2,0..99) = 99*100*199/6 = 328350
check_field "bigloop n=100 sum_of_squares=328350" \
"$BASE/stress-bigloop" '{"n":100}' "sum_of_squares" "328350"
# stress-divzero 42/7 = 6.0
check_field "divzero 42/7=6.0" \
"$BASE/stress-divzero" '{"n":42,"d":7}' "result" "6.0"
# divzero d=0 → 500
check_http "divzero d=0 → HTTP 500" \
"$BASE/stress-divzero" "POST" '{"n":1,"d":0}' "500"
# stress-writer: записывает 3 строки
check_field "writer rows=3 → count=3" \
"$BASE/stress-writer" '{"rows":3,"prefix":"functional-test"}' "count" "3"
check_field "writer rows=1 → count=1" \
"$BASE/stress-writer" '{"rows":1,"prefix":"functional-single"}' "count" "1"
# pg-stats
check_field "pg-stats version=v1-test7" \
"$BASE/pg-stats" "" "version" "v1-test7"
check_field_gt0 "pg-stats total_rows > 0" \
"$BASE/pg-stats" "" "total_rows"
# pg-info (nodejs)
check_field "pg-info runtime=nodejs20" \
"$BASE/pg-info" "" "runtime" "nodejs20"
# pg-table-reader
check_http "table-reader HTTP 200" "$BASE/pg-table-reader"
READER_RESP=$(call "$BASE/pg-table-reader")
READER_COUNT=$(echo "$READER_RESP" | python3 -c "
import sys, json
try:
d = json.load(sys.stdin)
print(d.get('count', 0))
except:
print(0)
" 2>/dev/null)
if [[ "$READER_COUNT" -gt 0 ]] 2>/dev/null; then
pass "table-reader count=$READER_COUNT строк"
else
fail "table-reader ожидали >0 строк, получили: $READER_COUNT | $(echo "$READER_RESP" | head -c 200)"
fi
# pg-table-writer: POST JSON должен вставить строку и вернуть JSON
info "pg-table-writer POST (ожидаем JSON если платформа инжектит _method)"
WRITER_RESP=$(curl -s -m 30 -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{"title":"full-test-insert-2026"}' \
"$BASE/pg-table-writer")
WRITER_OK=$(echo "$WRITER_RESP" | python3 -c "
import sys, json
try:
d = json.load(sys.stdin)
print(d.get('ok', False))
except:
print('NOT_JSON')
" 2>/dev/null)
if [[ "$WRITER_OK" == "True" ]]; then
pass "table-writer POST → ok=True, строка вставлена"
else
# HTML ответ — платформа не инжектит _method
info "table-writer вернул не JSON (вероятно HTML), ok=$WRITER_OK"
info "resp: $(echo "$WRITER_RESP" | head -c 100)"
# Это не баг, но фиксируем как наблюдение
fi
# ═══════════════════════════════════════════════════════════════
section "ФАЗА 3: PG-стресс (параллельная нагрузка на PostgreSQL)"
# ═══════════════════════════════════════════════════════════════
info "Запуск 40 параллельных stress-writer × 5 строк = 200 INSERT..."
ROWS_BEFORE=$(echo "$READER_COUNT")
WRITER_PIDS=()
for i in $(seq 1 40); do
curl -s -m 60 -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d "{\"rows\":5,\"prefix\":\"pgstorm-w$i\"}" \
"$BASE/stress-writer" > "/tmp/sw_$i.json" 2>&1 &
WRITER_PIDS+=($!)
done
wait "${WRITER_PIDS[@]}"
WRITER_OK=0; WRITER_FAIL=0
for i in $(seq 1 40); do
cnt=$(python3 -c "
import json
try:
d = json.load(open('/tmp/sw_$i.json'))
print(d.get('count', 0))
except:
print(0)
" 2>/dev/null)
if [[ "$cnt" == "5" ]]; then
((WRITER_OK++))
else
((WRITER_FAIL++))
info " writer batch $i: cnt=$cnt | $(cat /tmp/sw_$i.json | head -c 150)"
fi
done
info "writer: $WRITER_OK/40 OK, $WRITER_FAIL failed"
[[ "$WRITER_FAIL" == "0" ]] \
&& pass "40× parallel writer: все 40 вернули count=5 (200 строк)" \
|| fail "40× parallel writer: $WRITER_FAIL пакетов с ошибкой"
# Проверим что строки реально появились в таблице
NEW_COUNT=$(call "$BASE/pg-stats" | python3 -c "
import sys, json
try:
print(json.load(sys.stdin).get('total_rows', 0))
except:
print(0)
")
info "pg-stats: total_rows=$NEW_COUNT (было $ROWS_BEFORE до stress)"
[[ "$NEW_COUNT" -gt "$ROWS_BEFORE" ]] \
&& pass "pg-stats: строки выросли ($ROWS_BEFORE$NEW_COUNT)" \
|| fail "pg-stats: строки не выросли ($ROWS_BEFORE$NEW_COUNT)"
info "Запуск 30 параллельных stress-js-async (3 PG-запроса каждый = 90 одновременных)..."
JS_PIDS=()
for i in $(seq 1 30); do
curl -s -m 30 -H "Authorization: Bearer $TOKEN" \
"$BASE/stress-js-async" > "/tmp/jsa_$i.json" 2>&1 &
JS_PIDS+=($!)
done
wait "${JS_PIDS[@]}"
JS_OK=0; JS_FAIL=0
for i in $(seq 1 30); do
rt=$(python3 -c "
import json
try:
print(json.load(open('/tmp/jsa_$i.json')).get('runtime', 'err'))
except:
print('err')
" 2>/dev/null)
if [[ "$rt" == "nodejs20" ]]; then ((JS_OK++)); else ((JS_FAIL++)); fi
done
[[ "$JS_FAIL" == "0" ]] \
&& pass "30× parallel js-async: все 30 OK" \
|| fail "30× parallel js-async: $JS_OK ok, $JS_FAIL failed"
info "Запуск stress-go-pgstorm workers=50 duration=45s..."
PGSTORM_RESP=$(curl -s -m 120 \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"workers":50,"duration_sec":45,"max_delay_ms":50}' \
"$BASE/stress-go-pgstorm")
PGSTORM_OK=$(echo "$PGSTORM_RESP" | python3 -c "
import sys, json
try:
d = json.load(sys.stdin)
print(d.get('ok_ops', 0))
except:
print(0)
")
PGSTORM_ERR=$(echo "$PGSTORM_RESP" | python3 -c "
import sys, json
try:
d = json.load(sys.stdin)
print(d.get('err_ops', 0))
except:
print(-1)
")
PGSTORM_RPS=$(echo "$PGSTORM_RESP" | python3 -c "
import sys, json
try:
d = json.load(sys.stdin)
print(d.get('ops_per_sec', '?'))
except:
print('?')
")
info "pgstorm: ok=$PGSTORM_OK err=$PGSTORM_ERR ops/s=$PGSTORM_RPS"
[[ "$PGSTORM_OK" -gt 0 ]] 2>/dev/null \
&& pass "stress-go-pgstorm: $PGSTORM_OK ops OK, $PGSTORM_ERR err, $PGSTORM_RPS ops/s" \
|| fail "stress-go-pgstorm: 0 операций | $(echo "$PGSTORM_RESP" | head -c 300)"
# Итоговая статистика таблицы
FINAL_STATS=$(call "$BASE/pg-stats")
FINAL_ROWS=$(echo "$FINAL_STATS" | python3 -c "
import sys, json
try:
print(json.load(sys.stdin).get('total_rows', 0))
except:
print(0)
")
info "Итого строк в terraform_demo_table: $FINAL_ROWS"
# ═══════════════════════════════════════════════════════════════
section "ФАЗА 4: Краш-шторм (параллельные паники — платформа должна жить)"
# ═══════════════════════════════════════════════════════════════
info "25× го-nil crash + 25× divzero + 25× js-badenv = 75 параллельных крашей..."
CRASH_PIDS=()
for i in $(seq 1 25); do
curl -s -o /dev/null -w "%{http_code}" -m 15 \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"crash":true}' "$BASE/stress-go-nil" > "/tmp/c_nil_$i.txt" 2>&1 &
CRASH_PIDS+=($!)
curl -s -o /dev/null -w "%{http_code}" -m 15 \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"n":1,"d":0}' "$BASE/stress-divzero" > "/tmp/c_dz_$i.txt" 2>&1 &
CRASH_PIDS+=($!)
curl -s -o /dev/null -w "%{http_code}" -m 15 \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"crash":true}' "$BASE/stress-js-badenv" > "/tmp/c_js_$i.txt" 2>&1 &
CRASH_PIDS+=($!)
done
wait "${CRASH_PIDS[@]}"
C500=0; CNOT500=0
for i in $(seq 1 25); do
for f in "/tmp/c_nil_$i.txt" "/tmp/c_dz_$i.txt" "/tmp/c_js_$i.txt"; do
code=$(cat "$f" 2>/dev/null || echo "0")
if [[ "$code" == "500" ]]; then ((C500++)); else ((CNOT500++)); info " неожиданный $f: code=$code"; fi
done
done
info "Краши: $C500 × 500, $CNOT500 неожиданных"
[[ "$CNOT500" == "0" ]] \
&& pass "75× краш-шторм: все вернули HTTP 500 (платформа устойчива)" \
|| fail "75× краш-шторм: $CNOT500 ответов не 500"
info "Проверяем что сервисы живы после краш-шторма..."
check_http "go-fast: жив после штормов" "$BASE/stress-go-fast" "GET" "" "200"
check_http "js-async: жив после штормов" "$BASE/stress-js-async" "GET" "" "200"
check_http "pg-table-reader: жив после штормов" "$BASE/pg-table-reader" "GET" "" "200"
check_http "pg-stats: жив после штормов" "$BASE/pg-stats" "GET" "" "200"
# ═══════════════════════════════════════════════════════════════
section "ИТОГИ"
# ═══════════════════════════════════════════════════════════════
echo ""
TOTAL=$((PASS + FAIL))
echo -e " Всего тестов: $TOTAL"
echo -e " ${GREEN}PASS: $PASS${NC}"
echo -e " ${RED}FAIL: $FAIL${NC}"
echo ""
if [[ "$FAIL" == "0" ]]; then
echo -e " ${GREEN}✓ ВСЕ ТЕСТЫ ПРОШЛИ${NC}"
exit 0
else
echo -e " ${RED}✗ ЕСТЬ ПАДЕНИЯ ($FAIL)${NC}"
exit 1
fi
-129
View File
@@ -1,129 +0,0 @@
# 2026-03-18 (обновлено: фильтрация SLESS_EXCLUDE, читаемый вывод через "#"-ключ)
# funcs_list.py — HTTP-функция: список всех пользовательских функций с их статусами.
# Вызывает внутренний REST API оператора (ClusterIP, без TLS).
# Объединяет данные функций и триггеров в один ответ; скрывает служебные функции.
#
# Env vars:
# SLESS_API_URL — URL оператора (http://sless-operator.sless.svc.cluster.local:9090)
# SLESS_NAMESPACE — namespace пользователя (sless-{hex16})
# SLESS_TOKEN — JWT токен для /v1/ API
# SLESS_EXTERNAL_URL — публичный базовый URL (https://sless.kube5s.ru), для корректных ссылок
# SLESS_EXCLUDE — comma-separated имена функций, которые не надо показывать
# Пример: "funcs,event-writer,event-monitor,event-cleaner"
#
# Формат вывода: JSON-объект, где каждая функция содержит поле "#" — краткий комментарий.
# При pretty-print (python3 -m json.tool) выглядит как читаемый список с аннотациями.
import os
import requests
def _short_comment(fn, http_triggers, cron_triggers):
"""Генерирует однострочный комментарий-описание функции по её метаданным."""
phase = fn.get("phase", "")
runtime = fn.get("runtime", "")
if http_triggers:
active_str = "активна" if http_triggers[0].get("active") else "неактивна"
return f"HTTP endpoint ({runtime}) — {phase}, {active_str}"
elif cron_triggers:
schedule = cron_triggers[0].get("schedule", "?")
active_str = "активна" if cron_triggers[0].get("active") else "неактивна"
return f"Cron '{schedule}' ({runtime}) — {phase}, {active_str}"
else:
return f"Job/runner без триггера ({runtime}) — {phase}"
def list_all(event):
api_url = os.environ["SLESS_API_URL"].rstrip("/")
namespace = os.environ["SLESS_NAMESPACE"]
token = os.environ["SLESS_TOKEN"]
ext_url = os.environ.get("SLESS_EXTERNAL_URL", "").rstrip("/")
# Имена функций, которые не должны присутствовать в выводе.
# Включает саму себя ("funcs") и служебные функции других примеров.
exclude = {
n.strip()
for n in os.environ.get("SLESS_EXCLUDE", "").split(",")
if n.strip()
}
headers = {"Authorization": f"Bearer {token}"}
fns_resp = requests.get(
f"{api_url}/v1/namespaces/{namespace}/functions",
headers=headers,
timeout=10,
)
fns_resp.raise_for_status()
trs_resp = requests.get(
f"{api_url}/v1/namespaces/{namespace}/triggers",
headers=headers,
timeout=10,
)
trs_resp.raise_for_status()
# Индекс триггеров по имени функции
triggers_by_fn = {}
for tr in trs_resp.json():
fn_name = tr.get("function") or tr.get("functionRef")
if fn_name:
triggers_by_fn.setdefault(fn_name, []).append(tr)
result = []
for fn in fns_resp.json():
name = fn["name"]
if name in exclude:
continue
http_triggers = [
t for t in triggers_by_fn.get(name, []) if t.get("type") == "http"
]
cron_triggers = [
t for t in triggers_by_fn.get(name, []) if t.get("type") == "cron"
]
is_active = any(
t.get("enabled", True) and t.get("active", False)
for t in triggers_by_fn.get(name, [])
)
entry = {
# "#" — первый ключ: служит визуальным комментарием при pretty-print
"#": _short_comment(fn, http_triggers, cron_triggers),
"name": name,
"runtime": fn.get("runtime"),
"phase": fn.get("phase"),
"active": is_active,
}
# URL вычисляем из SLESS_EXTERNAL_URL если задан — state может хранить старый домен
if http_triggers:
if ext_url:
entry["url"] = f"{ext_url}/fn/{namespace}/{name}"
else:
entry["url"] = http_triggers[0].get("url", "")
if cron_triggers:
entry["cron"] = cron_triggers[0].get("schedule", "")
if fn.get("message"):
entry["message"] = fn["message"]
# created_at и last_built_at — доступны после обновления оператора до v0.1.32+
if fn.get("created_at"):
entry["created_at"] = fn["created_at"]
if fn.get("last_built_at"):
entry["last_built_at"] = fn["last_built_at"]
result.append(entry)
# Сортировка: активные вверх, затем по имени
result.sort(key=lambda f: (not f["active"], f["name"]))
return {
"namespace": namespace,
"count": len(result),
"functions": result,
}
-69
View File
@@ -1,69 +0,0 @@
# resource "nubes_lucee" "app1" {
# # Lucee-приложение, подключается к nubes_postgres.npg (наш Postgres в реалме var.realm).
# resource_name = "lucy_teststand_0"
# resource_realm = nubes_postgres.npg.resource_realm
# domain = "web-test-stand"
# git_path = "https://gitea-naeel.giteak8s.services.ngcloud.ru/naeel/testlucee"
# json_env = jsonencode({
# # Настройки Data Source 'testds' для Lucee (Application.cfc)
# testds_class = "org.postgresql.Driver"
# testds_bundleName = "org.postgresql.jdbc"
# testds_bundleVersion = "42.6.0"
# testds_connectionString = "jdbc:postgresql://${local.pg_host}:5432/${local.pg_database}?sslmode=require"
# testds_username = local.pg_username
# testds_password = local.pg_password
# testds_connectionLimit = "5"
# testds_liveTimeout = "15"
# testds_validate = "false"
# })
# resource_c_p_u = 300
# resource_memory = 512
# resource_instances = 1
# app_version = "5.4"
# depends_on = [nubes_postgres.npg]
# }
# resource "nubes_nodejs" "app3" {
# # NodeJS — застрял при создании на тест-стенде (операция в ожидании).
# # Раскомментировать когда тест-стенд стабилен.
# resource_name = "node_01"
# resource_realm = nubes_postgres.npg.resource_realm
# domain = "node07"
# git_path = "https://gitea-naeel.giteak8s.services.ngcloud.ru/naeel/testnode.git"
# health_path = "/healthz"
# app_version = "23"
#
# json_env = jsonencode({
# PGHOST = local.pg_host
# PGPORT = "5432"
# PGUSER = local.pg_username
# PGPASSWORD = local.pg_password
# PGDATABASE = local.pg_database
# PGSSLMODE = "require"
# DATABASE_URL = format(
# "postgresql://%s:%s@%s:5432/%s?sslmode=require",
# local.pg_username,
# local.pg_password,
# local.pg_host,
# local.pg_database
# )
# })
#
# resource_c_p_u = 300
# resource_memory = 256
# resource_instances = 1
#
# depends_on = [nubes_postgres.npg]
# }
# output "pg_vault_secrets" {
# value = nubes_postgres.db2.vault_secrets
# sensitive = true
# }
+167
View File
@@ -0,0 +1,167 @@
// 2026-03-21 — stress.tf: все стресс-сервисы для комплексного тестирования.
// Три рантайма: go1.23 (3), nodejs20 (2), python3.11 (5).
// Все depends_on = [sless_job.postgres_table_init_job] — таблица должна существовать.
# ── Go 1.23 ─────────────────────────────────────────────────────────────────
# Быстрая математика: факториал + числа Фибоначчи. Без PG. Проверяет Go runtime.
resource "sless_service" "stress_go_fast" {
name = "stress-go-fast"
runtime = "go1.23"
entrypoint = "handler.Handle"
memory_mb = 128
timeout_sec = 15
source_dir = "${path.module}/code/stress-go-fast"
depends_on = [sless_job.postgres_table_init_job]
}
# Намеренный nil pointer dereference. Без PG. Проверяет recover() в Go runtime.
resource "sless_service" "stress_go_nil" {
name = "stress-go-nil"
runtime = "go1.23"
entrypoint = "handler.Handle"
memory_mb = 128
timeout_sec = 10
source_dir = "${path.module}/code/stress-go-nil"
depends_on = [sless_job.postgres_table_init_job]
}
# Конкурентный PG-шторм через pgxpool: N горутин INSERT/COUNT/MAX.
# timeout_sec=660 — покрывает max duration_sec=600 с запасом.
# pgx/v5 уже в базовом образе go1.23: user go.mod не нужен.
resource "sless_service" "stress_go_pgstorm" {
name = "stress-go-pgstorm"
runtime = "go1.23"
entrypoint = "handler.Handle"
memory_mb = 256
timeout_sec = 660
source_dir = "${path.module}/code/stress-go-pgstorm"
env_vars = {
PGHOST = local.pg_host
PGPORT = "5432"
PGDATABASE = local.pg_database
PGUSER = local.pg_username
PGPASSWORD = local.pg_password
PGSSLMODE = "require"
}
depends_on = [sless_job.postgres_table_init_job]
}
# ── Node.js 20 ────────────────────────────────────────────────────────────────
# 3 параллельных PG-запроса через Promise.all. Проверяет async/await + nodejs20.
resource "sless_service" "stress_js_async" {
name = "stress-js-async"
runtime = "nodejs20"
entrypoint = "stress_js_async.run"
memory_mb = 128
timeout_sec = 20
source_dir = "${path.module}/code/stress-js-async"
env_vars = {
PGHOST = local.pg_host
PGPORT = "5432"
PGDATABASE = local.pg_database
PGUSER = local.pg_username
PGPASSWORD = local.pg_password
PGSSLMODE = "require"
}
depends_on = [sless_job.postgres_table_init_job]
}
# TypeError через undefined.toUpperCase(). Без PG. Проверяет перехват JS-ошибок.
resource "sless_service" "stress_js_badenv" {
name = "stress-js-badenv"
runtime = "nodejs20"
entrypoint = "stress_js_badenv.run"
memory_mb = 128
timeout_sec = 10
source_dir = "${path.module}/code/stress-js-badenv"
depends_on = [sless_job.postgres_table_init_job]
}
# ── Python 3.11 ───────────────────────────────────────────────────────────────
# Спит N секунд. Без PG. Проверяет timeout и сосуществование долгих запросов.
resource "sless_service" "stress_slow" {
name = "stress-slow"
runtime = "python3.11"
entrypoint = "stress_slow.run"
memory_mb = 128
timeout_sec = 35
source_dir = "${path.module}/code/stress-slow"
depends_on = [sless_job.postgres_table_init_job]
}
# CPU-нагрузка: сумма квадратов N чисел. Без PG. Проверяет compute-bound задачи.
resource "sless_service" "stress_bigloop" {
name = "stress-bigloop"
runtime = "python3.11"
entrypoint = "stress_bigloop.run"
memory_mb = 256
timeout_sec = 60
source_dir = "${path.module}/code/stress-bigloop"
depends_on = [sless_job.postgres_table_init_job]
}
# ZeroDivisionError. Без PG. Проверяет перехват Python-исключений → HTTP 500.
resource "sless_service" "stress_divzero" {
name = "stress-divzero"
runtime = "python3.11"
entrypoint = "stress_divzero.run"
memory_mb = 128
timeout_sec = 10
source_dir = "${path.module}/code/stress-divzero"
depends_on = [sless_job.postgres_table_init_job]
}
# Параллельный INSERT в terraform_demo_table через psycopg2. Проверяет PG-write.
resource "sless_service" "stress_writer" {
name = "stress-writer"
runtime = "python3.11"
entrypoint = "stress_writer.run"
memory_mb = 128
timeout_sec = 60
source_dir = "${path.module}/code/stress-writer"
env_vars = {
PGHOST = local.pg_host
PGPORT = "5432"
PGDATABASE = local.pg_database
PGUSER = local.pg_username
PGPASSWORD = local.pg_password
PGSSLMODE = "require"
}
depends_on = [sless_job.postgres_table_init_job]
}
# Агрегированная статистика terraform_demo_table (COUNT/MIN/MAX). Для мониторинга.
resource "sless_service" "pg_stats" {
name = "pg-stats"
runtime = "python3.11"
entrypoint = "pg_stats.get_stats"
memory_mb = 128
timeout_sec = 15
source_dir = "${path.module}/code/pg-stats"
env_vars = {
PGHOST = local.pg_host
PGPORT = "5432"
PGDATABASE = local.pg_database
PGUSER = local.pg_username
PGPASSWORD = local.pg_password
PGSSLMODE = "require"
}
depends_on = [sless_job.postgres_table_init_job]
}
+2 -2
View File
@@ -66,13 +66,13 @@ func PrepareContext(zipData []byte, runtime string) (*bytes.Buffer, error) {
func runtimeBaseImage(runtime string) (string, error) { func runtimeBaseImage(runtime string) (string, error) {
switch runtime { switch runtime {
case "python3.11": case "python3.11":
return "naeel/sless-runtime-python3.11:v0.1.4", nil return "naeel/sless-runtime-python3.11:v0.1.6", nil
case "nodejs20": case "nodejs20":
return "naeel/sless-runtime-nodejs20:v0.1.2", nil return "naeel/sless-runtime-nodejs20:v0.1.2", nil
case "go1.23": case "go1.23":
// Go использует builder-образ (содержит golang:1.23 + server.go + go.mod/go.sum с pgx/v5). // Go использует builder-образ (содержит golang:1.23 + server.go + go.mod/go.sum с pgx/v5).
// Конечный образ multi-stage — alpine без компилятора. // Конечный образ multi-stage — alpine без компилятора.
return "naeel/sless-runtime-go1.23:v0.1.1", nil return "naeel/sless-runtime-go1.23:v0.1.2", nil
default: default:
return "", fmt.Errorf("unsupported runtime: %q (supported: python3.11, nodejs20, go1.23)", runtime) return "", fmt.Errorf("unsupported runtime: %q (supported: python3.11, nodejs20, go1.23)", runtime)
} }
+72 -61
View File
@@ -1,4 +1,5 @@
// Создано: 2026-03-11 // Создано: 2026-03-11
// Изменено: 2026-03-21 (v0.1.2) — panic recovery: при panic в Handle() возвращаем HTTP 500 вместо EOF.
// HTTP-обёртка для serverless функций на Go 1.23. // HTTP-обёртка для serverless функций на Go 1.23.
// Компилируется kaniko ВМЕСТЕ с пользовательским кодом (package handler). // Компилируется kaniko ВМЕСТЕ с пользовательским кодом (package handler).
// //
@@ -16,71 +17,81 @@
package main package main
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"io" "io"
"log" "log"
"net/http" "net/http"
"os" "os"
"sless/fn/handler" "sless/fn/handler"
) )
func main() { func main() {
mode := os.Getenv("SLESS_MODE") mode := os.Getenv("SLESS_MODE")
// job-runner: разово вызвать Handle(event), вывести результат в JSON и выйти. // job-runner: разово вызвать Handle(event), вывести результат в JSON и выйти.
if mode == "job" { if mode == "job" {
eventJSON := os.Getenv("SLESS_EVENT") eventJSON := os.Getenv("SLESS_EVENT")
if eventJSON == "" { if eventJSON == "" {
eventJSON = "{}" eventJSON = "{}"
} }
var event map[string]interface{} var event map[string]interface{}
if err := json.Unmarshal([]byte(eventJSON), &event); err != nil { if err := json.Unmarshal([]byte(eventJSON), &event); err != nil {
event = map[string]interface{}{} event = map[string]interface{}{}
} }
result := handler.Handle(event) result := handler.Handle(event)
out, _ := json.Marshal(result) out, _ := json.Marshal(result)
fmt.Println(string(out)) fmt.Println(string(out))
return return
} }
// HTTP-сервер // HTTP-сервер
port := "8080" port := "8080"
http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) { http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
fmt.Fprint(w, `{"status":"ok"}`) fmt.Fprint(w, `{"status":"ok"}`)
}) })
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
event := map[string]interface{}{} // Перехватываем panic в пользовательском коде — возвращаем 500 вместо EOF.
if r.Method == http.MethodPost || r.Method == http.MethodPut { // Без recover() Go закрывает соединение при панике → ingress видит EOF → 502.
body, err := io.ReadAll(r.Body) defer func() {
if err == nil && len(body) > 0 { if rec := recover(); rec != nil {
_ = json.Unmarshal(body, &event) log.Printf("panic recovered: %v", rec)
} w.Header().Set("Content-Type", "application/json")
} w.WriteHeader(http.StatusInternalServerError)
event["_path"] = r.URL.Path fmt.Fprintf(w, `{"error":"panic: %v"}`, rec)
event["_method"] = r.Method }
if q := r.URL.Query(); len(q) > 0 { }()
qmap := map[string]interface{}{} event := map[string]interface{}{}
for k, v := range q { if r.Method == http.MethodPost || r.Method == http.MethodPut {
if len(v) == 1 { body, err := io.ReadAll(r.Body)
qmap[k] = v[0] if err == nil && len(body) > 0 {
} else { _ = json.Unmarshal(body, &event)
qmap[k] = v }
} }
} event["_path"] = r.URL.Path
event["_query"] = qmap event["_method"] = r.Method
} if q := r.URL.Query(); len(q) > 0 {
result := handler.Handle(event) qmap := map[string]interface{}{}
out, err := json.Marshal(result) for k, v := range q {
if err != nil { if len(v) == 1 {
http.Error(w, `{"error":"marshal failed"}`, 500) qmap[k] = v[0]
return } else {
} qmap[k] = v
w.Header().Set("Content-Type", "application/json") }
w.Write(out) }
}) event["_query"] = qmap
log.Printf("sless runtime (go1.23) listening on :%s", port) }
log.Fatal(http.ListenAndServe(":"+port, nil)) result := handler.Handle(event)
out, err := json.Marshal(result)
if err != nil {
http.Error(w, `{"error":"marshal failed"}`, 500)
return
}
w.Header().Set("Content-Type", "application/json")
w.Write(out)
})
log.Printf("sless runtime (go1.23) listening on :%s", port)
log.Fatal(http.ListenAndServe(":"+port, nil))
} }
+33 -6
View File
@@ -1,5 +1,10 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# Изменено: 2026-03-18 (string return → text/plain без json.dumps) # Изменено: 2026-03-18 (string return → text/plain без json.dumps)
# Изменено: 2026-03-21 (v0.1.5) — два фикса:
# 1. try/except вокруг вызова _handle() → HTTP 500 вместо EOF при исключениях
# 2. ThreadingHTTPServer вместо HTTPServer → concurrent requests (>5 параллельных)
# Изменено: 2026-03-21 (v0.1.6) — фикс 3:
# 3. _HighBacklogHTTPServer: request_queue_size=128 → listen(128) вместо listen(5) → нет RST при burst-нагрузке
# HTTP-обёртка для serverless функций на Python 3.11. # HTTP-обёртка для serverless функций на Python 3.11.
# Загружает модуль из SLESS_ENTRYPOINT или handler.py по умолчанию. # Загружает модуль из SLESS_ENTRYPOINT или handler.py по умолчанию.
# Формат SLESS_ENTRYPOINT: "module_name.func_name" (например: handler.handle) # Формат SLESS_ENTRYPOINT: "module_name.func_name" (например: handler.handle)
@@ -9,7 +14,8 @@ import sys
import os import os
import json import json
import importlib.util import importlib.util
from http.server import HTTPServer, BaseHTTPRequestHandler from http.server import HTTPServer, ThreadingHTTPServer, BaseHTTPRequestHandler
import traceback
from urllib.parse import urlparse, parse_qs from urllib.parse import urlparse, parse_qs
PORT = 8080 PORT = 8080
@@ -64,8 +70,14 @@ class FunctionHandler(BaseHTTPRequestHandler):
if self.path == "/health": if self.path == "/health":
self._respond(200, {"status": "ok"}) self._respond(200, {"status": "ok"})
else: else:
event = self._parse_request_meta({}) try:
self._respond(200, _handle(event)) event = self._parse_request_meta({})
self._respond(200, _handle(event))
except Exception as exc:
# Ловим любое исключение в пользовательском коде → 500 вместо EOF.
# Без try/except BaseHTTPServer закрывает соединение → ingress видит EOF → 502.
sys.stderr.write(traceback.format_exc())
self._respond(500, {"error": repr(exc)})
def _handle_with_body(self): def _handle_with_body(self):
# Общий обработчик для методов с телом (POST, PUT, PATCH, DELETE и др.) # Общий обработчик для методов с телом (POST, PUT, PATCH, DELETE и др.)
@@ -78,7 +90,11 @@ class FunctionHandler(BaseHTTPRequestHandler):
# Если тело не JSON — передаём как строку, не ломаем вызов # Если тело не JSON — передаём как строку, не ломаем вызов
event = {"body": body.decode("utf-8", errors="replace")} event = {"body": body.decode("utf-8", errors="replace")}
event = self._parse_request_meta(event) event = self._parse_request_meta(event)
self._respond(200, _handle(event)) try:
self._respond(200, _handle(event))
except Exception as exc:
sys.stderr.write(traceback.format_exc())
self._respond(500, {"error": repr(exc)})
def do_POST(self): def do_POST(self):
self._handle_with_body() self._handle_with_body()
@@ -94,7 +110,11 @@ class FunctionHandler(BaseHTTPRequestHandler):
if self.path == "/health": if self.path == "/health":
result = {"status": "ok"} result = {"status": "ok"}
else: else:
result = _handle(self._parse_request_meta({})) try:
result = _handle(self._parse_request_meta({}))
except Exception as exc:
sys.stderr.write(traceback.format_exc())
result = {"error": repr(exc)}
body = json.dumps(result).encode("utf-8") body = json.dumps(result).encode("utf-8")
self.send_response(200) self.send_response(200)
self.send_header("Content-Type", "application/json") self.send_header("Content-Type", "application/json")
@@ -129,7 +149,14 @@ class FunctionHandler(BaseHTTPRequestHandler):
self.wfile.write(body) self.wfile.write(body)
class _HighBacklogHTTPServer(ThreadingHTTPServer):
# request_queue_size задаёт аргумент listen(backlog) у TCP-сокета.
# Дефолт=5: при 40+ одновременных соединений ядро сбрасывает лишние (RST) → 502.
# 128 достаточно для burst-нагрузок без смены архитектуры.
request_queue_size = 128
if __name__ == "__main__": if __name__ == "__main__":
server = HTTPServer(("0.0.0.0", PORT), FunctionHandler) server = _HighBacklogHTTPServer(("0.0.0.0", PORT), FunctionHandler)
print(f"sless runtime (python3.11) listening on :{PORT}", flush=True) print(f"sless runtime (python3.11) listening on :{PORT}", flush=True)
server.serve_forever() server.serve_forever()