Files
Naeel 778cbc8b32 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 (паники не роняют платформу)
2026-03-21 08:42:03 +03:00

163 lines
8.0 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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)
# Почему importlib: нужно загружать модуль из фиксированного пути вне sys.path.
import sys
import os
import json
import importlib.util
from http.server import HTTPServer, ThreadingHTTPServer, BaseHTTPRequestHandler
import traceback
from urllib.parse import urlparse, parse_qs
PORT = 8080
# Разбираем SLESS_ENTRYPOINT="module.func" → файл и имя функции.
# Fallback: handler.py + handle — для обратной совместимости.
_entrypoint = os.environ.get("SLESS_ENTRYPOINT", "handler.handle")
_dot_idx = _entrypoint.rfind(".")
_module_name = _entrypoint[:_dot_idx] if _dot_idx >= 0 else _entrypoint
_func_name = _entrypoint[_dot_idx + 1:] if _dot_idx >= 0 else "handle"
HANDLER_PATH = f"/app/function/{_module_name}.py"
def load_handler():
# Загружаем модуль пользователя динамически — путь известен только в runtime.
spec = importlib.util.spec_from_file_location(_module_name, HANDLER_PATH)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
if not hasattr(module, _func_name):
raise AttributeError(f"{HANDLER_PATH} must define a '{_func_name}(event)' function")
return getattr(module, _func_name)
_handle = load_handler()
class FunctionHandler(BaseHTTPRequestHandler):
def log_message(self, format, *args):
# Логи в stderr чтобы не мешать stdout
sys.stderr.write("%s - [%s] %s\n" % (
self.address_string(),
self.log_date_time_string(),
format % args,
))
def _parse_request_meta(self, event):
# Добавляем _path, _query, _method в event — позволяет роутить внутри функции.
# Например: /fn/ns/notes/add?title=x → event._path='/add', event._query={'title':'x'}
# Зарезервированные поля, не конфликтуют с бизнес-полями.
parsed = urlparse(self.path)
query = {k: v[0] if len(v) == 1 else v
for k, v in parse_qs(parsed.query).items()}
if isinstance(event, dict):
event['_path'] = parsed.path
event['_query'] = query
event['_method'] = self.command
# Accept заголовок — позволяет функции различать браузер и curl/API.
event['_accept'] = self.headers.get('Accept', '')
return event
def do_GET(self):
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 и др.)
# Читаем тело только если Content-Length > 0, иначе передаём пустой event.
length = int(self.headers.get("Content-Length", 0))
body = self.rfile.read(length) if length > 0 else b""
try:
event = json.loads(body) if body else {}
except json.JSONDecodeError:
# Если тело не 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()
# PUT, DELETE, PATCH — передаём в функцию как обычные вызовы.
# event['_method'] позволяет функции роутить по методу внутри.
do_PUT = _handle_with_body
do_DELETE = _handle_with_body
do_PATCH = _handle_with_body
def do_HEAD(self):
# HEAD: те же заголовки что и GET, но без тела (HTTP-стандарт).
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")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
# Тело не отправляем — семантика HEAD
def send_error(self, code, message=None, explain=None):
# Переопределяем дефолтный HTML-ответ на JSON — пользователь всегда получает JSON.
body = json.dumps({"error": message or f"HTTP {code}", "code": code}).encode("utf-8")
self.send_response(code)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
def _respond(self, status, data):
# Если функция вернула строку:
# начинается с '<' → HTML (text/html) — для страниц с формами.
# иначе → text/plain — для человекочитаемого текста.
# Позволяет функциям возвращать HTML напрямую без изменений runtime-образа.
if isinstance(data, str):
body = data.encode("utf-8")
ctype = "text/html; charset=utf-8" if data.lstrip().startswith("<") else "text/plain; charset=utf-8"
else:
body = json.dumps(data).encode("utf-8")
ctype = "application/json"
self.send_response(status)
self.send_header("Content-Type", ctype)
self.send_header("Content-Length", str(len(body)))
self.end_headers()
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 = _HighBacklogHTTPServer(("0.0.0.0", PORT), FunctionHandler)
print(f"sless runtime (python3.11) listening on :{PORT}", flush=True)
server.serve_forever()