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:
@@ -1,9 +1,9 @@
|
||||
# Изменено: 2026-03-11
|
||||
# Изменено: 2026-03-21
|
||||
# Деплой sless оператора в кластер.
|
||||
# Состав:
|
||||
# - ConfigMap: не-секретные env vars (S3_ENDPOINT, REGISTRY_HOST и т.д.)
|
||||
# - 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)
|
||||
# - Ingress: sless.kube5s.ru → :9090 (внешний доступ с TLS)
|
||||
#
|
||||
@@ -74,7 +74,7 @@ spec:
|
||||
containers:
|
||||
- name: operator
|
||||
# При обновлении версии оператора — менять тег здесь (не 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 — чтобы всегда тянуть по точному тегу (не кешировать старый)
|
||||
imagePullPolicy: Always
|
||||
ports:
|
||||
|
||||
@@ -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
|
||||
// handler.go — быстрая Go функция: факториал + числа Фибоначчи.
|
||||
// Проверяет Go runtime под лёгкой нагрузкой и корректность JSON-ответа.
|
||||
|
||||
Executable
+437
@@ -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
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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
|
||||
# }
|
||||
|
||||
|
||||
@@ -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]
|
||||
}
|
||||
@@ -66,13 +66,13 @@ func PrepareContext(zipData []byte, runtime string) (*bytes.Buffer, error) {
|
||||
func runtimeBaseImage(runtime string) (string, error) {
|
||||
switch runtime {
|
||||
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":
|
||||
return "naeel/sless-runtime-nodejs20:v0.1.2", nil
|
||||
case "go1.23":
|
||||
// Go использует builder-образ (содержит golang:1.23 + server.go + go.mod/go.sum с pgx/v5).
|
||||
// Конечный образ multi-stage — alpine без компилятора.
|
||||
return "naeel/sless-runtime-go1.23:v0.1.1", nil
|
||||
return "naeel/sless-runtime-go1.23:v0.1.2", nil
|
||||
default:
|
||||
return "", fmt.Errorf("unsupported runtime: %q (supported: python3.11, nodejs20, go1.23)", runtime)
|
||||
}
|
||||
|
||||
+72
-61
@@ -1,4 +1,5 @@
|
||||
// Создано: 2026-03-11
|
||||
// Изменено: 2026-03-21 (v0.1.2) — panic recovery: при panic в Handle() возвращаем HTTP 500 вместо EOF.
|
||||
// HTTP-обёртка для serverless функций на Go 1.23.
|
||||
// Компилируется kaniko ВМЕСТЕ с пользовательским кодом (package handler).
|
||||
//
|
||||
@@ -16,71 +17,81 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"sless/fn/handler"
|
||||
"sless/fn/handler"
|
||||
)
|
||||
|
||||
func main() {
|
||||
mode := os.Getenv("SLESS_MODE")
|
||||
mode := os.Getenv("SLESS_MODE")
|
||||
|
||||
// job-runner: разово вызвать Handle(event), вывести результат в JSON и выйти.
|
||||
if mode == "job" {
|
||||
eventJSON := os.Getenv("SLESS_EVENT")
|
||||
if eventJSON == "" {
|
||||
eventJSON = "{}"
|
||||
}
|
||||
var event map[string]interface{}
|
||||
if err := json.Unmarshal([]byte(eventJSON), &event); err != nil {
|
||||
event = map[string]interface{}{}
|
||||
}
|
||||
result := handler.Handle(event)
|
||||
out, _ := json.Marshal(result)
|
||||
fmt.Println(string(out))
|
||||
return
|
||||
}
|
||||
// job-runner: разово вызвать Handle(event), вывести результат в JSON и выйти.
|
||||
if mode == "job" {
|
||||
eventJSON := os.Getenv("SLESS_EVENT")
|
||||
if eventJSON == "" {
|
||||
eventJSON = "{}"
|
||||
}
|
||||
var event map[string]interface{}
|
||||
if err := json.Unmarshal([]byte(eventJSON), &event); err != nil {
|
||||
event = map[string]interface{}{}
|
||||
}
|
||||
result := handler.Handle(event)
|
||||
out, _ := json.Marshal(result)
|
||||
fmt.Println(string(out))
|
||||
return
|
||||
}
|
||||
|
||||
// HTTP-сервер
|
||||
port := "8080"
|
||||
http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
fmt.Fprint(w, `{"status":"ok"}`)
|
||||
})
|
||||
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
event := map[string]interface{}{}
|
||||
if r.Method == http.MethodPost || r.Method == http.MethodPut {
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err == nil && len(body) > 0 {
|
||||
_ = json.Unmarshal(body, &event)
|
||||
}
|
||||
}
|
||||
event["_path"] = r.URL.Path
|
||||
event["_method"] = r.Method
|
||||
if q := r.URL.Query(); len(q) > 0 {
|
||||
qmap := map[string]interface{}{}
|
||||
for k, v := range q {
|
||||
if len(v) == 1 {
|
||||
qmap[k] = v[0]
|
||||
} else {
|
||||
qmap[k] = v
|
||||
}
|
||||
}
|
||||
event["_query"] = qmap
|
||||
}
|
||||
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))
|
||||
// HTTP-сервер
|
||||
port := "8080"
|
||||
http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
fmt.Fprint(w, `{"status":"ok"}`)
|
||||
})
|
||||
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
// Перехватываем panic в пользовательском коде — возвращаем 500 вместо EOF.
|
||||
// Без recover() Go закрывает соединение при панике → ingress видит EOF → 502.
|
||||
defer func() {
|
||||
if rec := recover(); rec != nil {
|
||||
log.Printf("panic recovered: %v", rec)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
fmt.Fprintf(w, `{"error":"panic: %v"}`, rec)
|
||||
}
|
||||
}()
|
||||
event := map[string]interface{}{}
|
||||
if r.Method == http.MethodPost || r.Method == http.MethodPut {
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err == nil && len(body) > 0 {
|
||||
_ = json.Unmarshal(body, &event)
|
||||
}
|
||||
}
|
||||
event["_path"] = r.URL.Path
|
||||
event["_method"] = r.Method
|
||||
if q := r.URL.Query(); len(q) > 0 {
|
||||
qmap := map[string]interface{}{}
|
||||
for k, v := range q {
|
||||
if len(v) == 1 {
|
||||
qmap[k] = v[0]
|
||||
} else {
|
||||
qmap[k] = v
|
||||
}
|
||||
}
|
||||
event["_query"] = qmap
|
||||
}
|
||||
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))
|
||||
}
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
#!/usr/bin/env python3
|
||||
# Изменено: 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.
|
||||
# Загружает модуль из SLESS_ENTRYPOINT или handler.py по умолчанию.
|
||||
# Формат SLESS_ENTRYPOINT: "module_name.func_name" (например: handler.handle)
|
||||
@@ -9,7 +14,8 @@ import sys
|
||||
import os
|
||||
import json
|
||||
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
|
||||
|
||||
PORT = 8080
|
||||
@@ -64,8 +70,14 @@ class FunctionHandler(BaseHTTPRequestHandler):
|
||||
if self.path == "/health":
|
||||
self._respond(200, {"status": "ok"})
|
||||
else:
|
||||
try:
|
||||
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):
|
||||
# Общий обработчик для методов с телом (POST, PUT, PATCH, DELETE и др.)
|
||||
@@ -78,7 +90,11 @@ class FunctionHandler(BaseHTTPRequestHandler):
|
||||
# Если тело не JSON — передаём как строку, не ломаем вызов
|
||||
event = {"body": body.decode("utf-8", errors="replace")}
|
||||
event = self._parse_request_meta(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):
|
||||
self._handle_with_body()
|
||||
@@ -94,7 +110,11 @@ class FunctionHandler(BaseHTTPRequestHandler):
|
||||
if self.path == "/health":
|
||||
result = {"status": "ok"}
|
||||
else:
|
||||
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")
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Type", "application/json")
|
||||
@@ -129,7 +149,14 @@ class FunctionHandler(BaseHTTPRequestHandler):
|
||||
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__":
|
||||
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)
|
||||
server.serve_forever()
|
||||
|
||||
Reference in New Issue
Block a user