From 778cbc8b32c514a44cdc4b2c9cd6b365579cfb50 Mon Sep 17 00:00:00 2001 From: Naeel Date: Sat, 21 Mar 2026 08:42:03 +0300 Subject: [PATCH] =?UTF-8?q?fix(runtime):=20Go=20panic=E2=86=92500=20(recov?= =?UTF-8?q?er),=20Python=20exception=E2=86=92500+threading+backlog?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 (паники не роняют платформу) --- deployments/k8s/operator.yaml | 6 +- examples/POSTGRES/README.md | 100 ++++ .../POSTGRES/code/stress-go-fast/handler.go | 1 - examples/POSTGRES/full_test.sh | 437 ++++++++++++++++++ examples/POSTGRES/funcs_list.py | 129 ------ examples/POSTGRES/luceUNDnode.tf | 69 --- examples/POSTGRES/stress.tf | 167 +++++++ ...ly.sh => stress_destroy_apply.sh.disabled} | 0 internal/builder/context.go | 4 +- runtimes/go1.23/server.go | 133 +++--- runtimes/python3.11/server.py | 39 +- 11 files changed, 814 insertions(+), 271 deletions(-) create mode 100644 examples/POSTGRES/README.md create mode 100755 examples/POSTGRES/full_test.sh delete mode 100644 examples/POSTGRES/funcs_list.py delete mode 100644 examples/POSTGRES/luceUNDnode.tf create mode 100644 examples/POSTGRES/stress.tf rename examples/POSTGRES/{stress_destroy_apply.sh => stress_destroy_apply.sh.disabled} (100%) diff --git a/deployments/k8s/operator.yaml b/deployments/k8s/operator.yaml index 78eba8d..326e5a0 100644 --- a/deployments/k8s/operator.yaml +++ b/deployments/k8s/operator.yaml @@ -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: diff --git a/examples/POSTGRES/README.md b/examples/POSTGRES/README.md new file mode 100644 index 0000000..ea5d451 --- /dev/null +++ b/examples/POSTGRES/README.md @@ -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 в определённых сценариях. diff --git a/examples/POSTGRES/code/stress-go-fast/handler.go b/examples/POSTGRES/code/stress-go-fast/handler.go index cd5ac46..9f40709 100644 --- a/examples/POSTGRES/code/stress-go-fast/handler.go +++ b/examples/POSTGRES/code/stress-go-fast/handler.go @@ -1,4 +1,3 @@ -package handler // 2026-03-19 // handler.go — быстрая Go функция: факториал + числа Фибоначчи. // Проверяет Go runtime под лёгкой нагрузкой и корректность JSON-ответа. diff --git a/examples/POSTGRES/full_test.sh b/examples/POSTGRES/full_test.sh new file mode 100755 index 0000000..917c8f9 --- /dev/null +++ b/examples/POSTGRES/full_test.sh @@ -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 diff --git a/examples/POSTGRES/funcs_list.py b/examples/POSTGRES/funcs_list.py deleted file mode 100644 index bc17fdc..0000000 --- a/examples/POSTGRES/funcs_list.py +++ /dev/null @@ -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, - } - diff --git a/examples/POSTGRES/luceUNDnode.tf b/examples/POSTGRES/luceUNDnode.tf deleted file mode 100644 index 8027a2f..0000000 --- a/examples/POSTGRES/luceUNDnode.tf +++ /dev/null @@ -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 -# } - - diff --git a/examples/POSTGRES/stress.tf b/examples/POSTGRES/stress.tf new file mode 100644 index 0000000..b31d449 --- /dev/null +++ b/examples/POSTGRES/stress.tf @@ -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] +} diff --git a/examples/POSTGRES/stress_destroy_apply.sh b/examples/POSTGRES/stress_destroy_apply.sh.disabled similarity index 100% rename from examples/POSTGRES/stress_destroy_apply.sh rename to examples/POSTGRES/stress_destroy_apply.sh.disabled diff --git a/internal/builder/context.go b/internal/builder/context.go index 52270ee..ec62116 100644 --- a/internal/builder/context.go +++ b/internal/builder/context.go @@ -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) } diff --git a/runtimes/go1.23/server.go b/runtimes/go1.23/server.go index ba74e8b..71b64bd 100644 --- a/runtimes/go1.23/server.go +++ b/runtimes/go1.23/server.go @@ -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)) } diff --git a/runtimes/python3.11/server.py b/runtimes/python3.11/server.py index ea38756..6f2c852 100644 --- a/runtimes/python3.11/server.py +++ b/runtimes/python3.11/server.py @@ -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: - event = self._parse_request_meta({}) - self._respond(200, _handle(event)) + 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) - 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): self._handle_with_body() @@ -94,7 +110,11 @@ class FunctionHandler(BaseHTTPRequestHandler): if self.path == "/health": result = {"status": "ok"} 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") 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()