diff --git a/FIX_ARCHIVE_PRUNER.md b/FIX_ARCHIVE_PRUNER.md new file mode 100644 index 0000000..cdb386e --- /dev/null +++ b/FIX_ARCHIVE_PRUNER.md @@ -0,0 +1,113 @@ +# FIX: archivePruner удаляет архивы tenant namespaces + +## Симптом + +Node.js функции не специализируются → fetcher 404 при скачивании архива. +Storagesvc логи: `archive_pruner` удаляет архив через ~90 сек после создания функции. + +## Причина + +`pkg/storagesvc/archivePruner.go`, функция `getOrphanArchives`, строка 95: + +```go +for _, namespace := range utils.DefaultNSResolver().Snapshot() { + pkgList, err := pruner.crdClient.CoreV1().Packages(namespace).List(...) +``` + +`DefaultNSResolver().Snapshot()` возвращает только namespaces зарегистрированные +через `AddNamespace()`. Storagesvc не слушает namespace events → tenant namespaces +(`fission-*`) туда не попадают → их Package CRD невидимы → все загруженные архивы +считаются orphan → удаляются. + +## Правка + +**Файл:** `pkg/storagesvc/archivePruner.go` + +**Заменить** (строки 94–125): + +```go + // get all pkgs from kubernetes + for _, namespace := range utils.DefaultNSResolver().Snapshot() { + pkgList, err := pruner.crdClient.CoreV1().Packages(namespace).List(ctx, metav1.ListOptions{}) + if err != nil { + pruner.logger.Error("error getting package list from kubernetes", zap.Error(err)) + return + } + + // extract archives referenced by these pkgs + for _, pkg := range pkgList.Items { + if pkg.Spec.Deployment.URL != "" { + archiveID, err = getQueryParamValue(pkg.Spec.Deployment.URL, "id") + if err != nil { + pruner.logger.Error("error extracting value of archiveID from deployment url", + zap.Error(err), + zap.String("url", pkg.Spec.Deployment.URL)) + return + } + archivesRefByPkgs = append(archivesRefByPkgs, archiveID) + } + if pkg.Spec.Source.URL != "" { + archiveID, err = getQueryParamValue(pkg.Spec.Source.URL, "id") + if err != nil { + pruner.logger.Error("error extracting value of archiveID from source url", + zap.Error(err), + zap.String("url", pkg.Spec.Source.URL)) + return + } + archivesRefByPkgs = append(archivesRefByPkgs, archiveID) + } + } + } +``` + +**На:** + +```go + // get all pkgs from kubernetes across ALL namespaces (including tenant namespaces) + // Fix: DefaultNSResolver().Snapshot() returns only fission-registered namespaces, + // not tenant namespaces (fission-*). Use NamespaceAll to scan everything. + pkgList, err := pruner.crdClient.CoreV1().Packages(metav1.NamespaceAll).List(ctx, metav1.ListOptions{}) + if err != nil { + pruner.logger.Error("error getting package list from kubernetes", zap.Error(err)) + return + } + + // extract archives referenced by these pkgs + for _, pkg := range pkgList.Items { + if pkg.Spec.Deployment.URL != "" { + archiveID, err = getQueryParamValue(pkg.Spec.Deployment.URL, "id") + if err != nil { + pruner.logger.Error("error extracting value of archiveID from deployment url", + zap.Error(err), + zap.String("url", pkg.Spec.Deployment.URL)) + return + } + archivesRefByPkgs = append(archivesRefByPkgs, archiveID) + } + if pkg.Spec.Source.URL != "" { + archiveID, err = getQueryParamValue(pkg.Spec.Source.URL, "id") + if err != nil { + pruner.logger.Error("error extracting value of archiveID from source url", + zap.Error(err), + zap.String("url", pkg.Spec.Source.URL)) + return + } + archivesRefByPkgs = append(archivesRefByPkgs, archiveID) + } + } +``` + +**Импорты** — удалить `"github.com/fission/fission/pkg/utils"` (больше не используется). + +## После правки + +```bash +cd ~/terra/fission-src +go build ./pkg/storagesvc/... # проверить компиляцию + +# пересобрать storagesvc образ и передеплоить +docker build -t naeel/fission-storagesvc:<новый-тег> -f cmd/storagesvc/Dockerfile . +docker push naeel/fission-storagesvc:<новый-тег> +kubectl set image -n fission deploy/storagesvc storagesvc=naeel/fission-storagesvc:<новый-тег> +kubectl rollout status -n fission deploy/storagesvc +``` diff --git a/LLM/LLM_PROMPT_TRAINING_2026-04-28.md b/LLM/LLM_PROMPT_TRAINING_2026-04-28.md index 6fad3ec..4822e25 100644 --- a/LLM/LLM_PROMPT_TRAINING_2026-04-28.md +++ b/LLM/LLM_PROMPT_TRAINING_2026-04-28.md @@ -235,3 +235,59 @@ ### Вывод - Текущий prompt уже работает на коротких и простых кейсах. - Следующий шаг — проверить его на более разнообразных задачах, чтобы убедиться, что шаблон не ломается за пределами приветствий. + +--- + +## Итерация 9 (2026-05-19) — RUNTIME BUG DISCOVERY + +### Источник +Реальный прогон `test_heavy.sh` на кластере. Обнаружены критические баги в PHP и Node.js runtime-контракте. + +### БАГ C: PHP `return array` → пустой response + +**Задача LLM:** написать PHP-функцию. +**Сгенерированный код (типичный):** +```php + 200, 'body' => json_encode(['tag' => 'ok'])]; +} +``` +**Результат invoke:** `{"status":200,"response_raw":""}` — тело пустое. + +**Причина:** PHP env (`ghcr.io/fission/php-env:latest`, server.php) создаёт `$response = new Response()`, +вызывает функцию и возвращает оригинальный объект. Return value функции игнорируется. + +**Правильный код:** +```php +getBody()->write(json_encode(['tag' => 'ok'])); +} +``` + +**Что изменено в промпте:** +- PHP секция переписана: запрет на `return array`, требование писать через `$ctx["response"]->getBody()->write(...)`. +- Добавлен пример с JSON-ответом и PSR-7 объектами. + +--- + +### БАГ B: Node.js entrypoint `"main.js"` — split на точке → funcname="js" + +**Проблема консоли, не пользовательского кода.** + +Node-env `specializeV2` (строка 160 server.js) интерпретирует functionName через `split(".")`: +- `"main.js"` → `{filename:"main", funcname:"js"}` → ищет `module["js"]` → undefined → specialize 500. +- `"main"` → `{filename:"main", funcname:undefined}` → берёт `default export` → работает. + +**Fix в консоли:** `console/internal/runtime/entrypoint.go`, `DefaultEntrypoint("nodejs")` = `"main"`. + +**Влияние на промпт:** никакого — это внутренняя проблема консоли. Пользовательский Node.js код пишется как обычно. + +--- + +### Итог итерации 9 +- PHP промпт обновлён: PSR-7 `getBody()->write()` вместо `return array`. +- Node.js промпт уточнён: `module.exports = async function(ctx) {...}`, CJS стиль. +- Документация: `doc/RUNTIME_FUNCTION_GUIDE.md` — полный справочник контрактов. + diff --git a/LLM/LLM_PROMPT_UNIVERSAL_DRAFT_2026-04-28.md b/LLM/LLM_PROMPT_UNIVERSAL_DRAFT_2026-04-28.md index e6320b8..5b6b36f 100644 --- a/LLM/LLM_PROMPT_UNIVERSAL_DRAFT_2026-04-28.md +++ b/LLM/LLM_PROMPT_UNIVERSAL_DRAFT_2026-04-28.md @@ -54,9 +54,31 @@ - Не ломай контракт функции выдуманными обертками или неподходящими типами. ### PHP -- Возвращай корректный PHP-код функции. -- Следи за открывающим тегом и синтаксисом массива/строк. -- Не выдумывай несуществующие `Fission\\Contract` классы или SDK, если задача про обычную функцию. +- **КРИТИЧНО:** PHP env НЕ читает return value функции. `return ['statusCode'=>200,'body'=>...]` — тело ответа будет ПУСТЫМ. +- Запись в ответ — ТОЛЬКО через `$ctx["response"]->getBody()->write(...)`: + ```php + getBody()->write(json_encode(['key' => 'value'])); + } + ``` +- Сигнатура: `function handler($ctx)` — без type hint `array` и без `: array` return type. +- Доступ к запросу: `$ctx["request"]`, к логгеру: `$ctx["logger"]`. +- Открывающий тег ` 'value']); + ``` + +### Node.js +- Пользовательский код оборачивается консолью в CJS wrapper. Писать как обычный CommonJS. +- Экспорт: `module.exports = async function(ctx) { return { status: 200, body: "..." }; }` +- Формат ответа: объект `{ status: число, body: строка }`. +- **НЕ** использовать `export default` — код запускается в CJS контексте. +- **НЕ** указывать `.js` в entrypoint — это внутренняя настройка консоли, не пользовательский код. +- Не вставляй ESM import/export обвязку в пользовательский код. ### Ruby - Возвращай корректный Ruby-код функции. diff --git a/bench-func/bench_func_test.sh b/bench-func/bench_func_test.sh new file mode 100644 index 0000000..6f4ddc4 --- /dev/null +++ b/bench-func/bench_func_test.sh @@ -0,0 +1,114 @@ +#!/bin/bash +# Бенчмарк: upload, deploy, cold/hot start для простой функции +# Все результаты пишутся в test-results/bench-func + +set -e +RESULTS_DIR="$(dirname "$0")/../test-results/bench-func" +FUNC_DIR="$(dirname "$0")" +FUNC_NAME="bench-hello" +ENV_NAME="bench-python-env" +NS="bench-func-$(date +%s)" +# Donor namespace с готовым RBAC +DONOR_NS=$(kubectl get ns --no-headers | awk '{print $1}' | grep -E '^fission-[a-f0-9]{16}$' | head -1) +LOGFILE="$RESULTS_DIR/bench_func_$(date +%Y-%m-%d_%H-%M-%S).log" + +mkdir -p "$RESULTS_DIR" +{ + echo "# Bench: $(date)" + echo "Namespace: $NS" + echo "Donor NS: $DONOR_NS" +} | tee "$LOGFILE" + +# 1. Create namespace + copy RBAC from donor +echo -n "[1] Create namespace + RBAC... " | tee -a "$LOGFILE" +START_NS=$(date +%s%3N) +kubectl create ns "$NS" >/dev/null + +# Копируем ServiceAccount-ы +for sa in fission-builder fission-fetcher; do + kubectl get sa "$sa" -n "$DONOR_NS" -o json 2>/dev/null \ + | jq 'del(.metadata.resourceVersion,.metadata.uid,.metadata.creationTimestamp,.metadata.annotations,.metadata.ownerReferences) | .metadata.namespace = $ns' --arg ns "$NS" \ + | kubectl apply -f - >/dev/null 2>&1 || true +done + +# Копируем RoleBindings из donor +kubectl get rolebinding -n "$DONOR_NS" -o json 2>/dev/null \ + | jq -r '.items[] | select(.roleRef.kind=="ClusterRole") | del(.metadata.resourceVersion,.metadata.uid,.metadata.creationTimestamp,.metadata.annotations,.metadata.ownerReferences) | .metadata.namespace = $ns' --arg ns "$NS" \ + | kubectl apply -f - >/dev/null 2>&1 || true + +END_NS=$(date +%s%3N) +echo "$((END_NS-START_NS)) ms" | tee -a "$LOGFILE" + +# 2. Create environment +echo -n "[2] Create environment... " | tee -a "$LOGFILE" +START_ENV=$(date +%s%3N) +fission env create --name "$ENV_NAME" --image naeel/fission-python-env:v1.1 --namespace "$NS" >/dev/null 2>&1 +END_ENV=$(date +%s%3N) +echo "$((END_ENV-START_ENV)) ms" | tee -a "$LOGFILE" + +# 3. Upload function +echo -n "[3] Upload function... " | tee -a "$LOGFILE" +START_UP=$(date +%s%3N) +fission fn create --name "$FUNC_NAME" --env "$ENV_NAME" --code "$FUNC_DIR/hello.py" --namespace "$NS" >/dev/null 2>&1 +END_UP=$(date +%s%3N) +echo "$((END_UP-START_UP)) ms" | tee -a "$LOGFILE" + +# 4. Cold start invoke (retry до 60 раз по 2 сек = 2 мин максимум) +echo -n "[4] Cold start invoke... " | tee -a "$LOGFILE" +START_COLD=$(date +%s%3N) +COLD_OK=0 +for i in {1..60}; do + RESULT=$(fission fn test --name "$FUNC_NAME" --namespace "$NS" 2>&1 || true) + if echo "$RESULT" | grep -qE "hello|200|statusCode"; then + COLD_OK=1 + break + fi + sleep 2 +done +END_COLD=$(date +%s%3N) +if [[ $COLD_OK -eq 1 ]]; then + echo "$((END_COLD-START_COLD)) ms" | tee -a "$LOGFILE" +else + echo "FAILED" | tee -a "$LOGFILE" + { + echo "[DEBUG] pods in $NS:" + kubectl get pods -n "$NS" 2>&1 + echo "[DEBUG] events in $NS:" + kubectl get events -n "$NS" --sort-by=.metadata.creationTimestamp 2>&1 | tail -20 + } >> "$LOGFILE" + exit 1 +fi + +# 5. Hot start invoke (pod уже прогрет, retry до 5 раз) +echo -n "[5] Hot start invoke... " | tee -a "$LOGFILE" +START_HOT=$(date +%s%3N) +HOT_OK=0 +for i in {1..5}; do + RESULT=$(fission fn test --name "$FUNC_NAME" --namespace "$NS" 2>&1 || true) + if echo "$RESULT" | grep -qE "hello|200|statusCode"; then + HOT_OK=1; break + fi + sleep 2 +done +END_HOT=$(date +%s%3N) +if [[ $HOT_OK -eq 1 ]]; then + echo "$((END_HOT-START_HOT)) ms" | tee -a "$LOGFILE" +else + echo "FAILED (pod не ответил)" | tee -a "$LOGFILE" +fi + +# 6. Hot x5 +echo "[6] Hot start x5:" | tee -a "$LOGFILE" +for i in {1..5}; do + START_H=$(date +%s%3N) + RESULT=$(fission fn test --name "$FUNC_NAME" --namespace "$NS" 2>&1 || true) + END_H=$(date +%s%3N) + if echo "$RESULT" | grep -qE "hello|200|statusCode"; then + echo " invoke #$i: $((END_H-START_H)) ms" | tee -a "$LOGFILE" + else + echo " invoke #$i: FAILED" | tee -a "$LOGFILE" + fi +done + +echo "---" | tee -a "$LOGFILE" +echo "DONE. Log: $LOGFILE" diff --git a/bench-func/hello.py b/bench-func/hello.py new file mode 100644 index 0000000..4295f22 --- /dev/null +++ b/bench-func/hello.py @@ -0,0 +1,5 @@ +def main(event, context): + return { + "statusCode": 200, + "body": "hello, world!" + } diff --git a/doc/RUNTIME_FUNCTION_GUIDE.md b/doc/RUNTIME_FUNCTION_GUIDE.md new file mode 100644 index 0000000..f93f8b4 --- /dev/null +++ b/doc/RUNTIME_FUNCTION_GUIDE.md @@ -0,0 +1,339 @@ +# Руководство: контракт функций в Fission runtime + +Дата: 2026-05-19 +Версия консоли: v1.3.94+ +Источник: дизассемблирование реальных env-образов в кластере + отладка тестов + +--- + +## Содержание + +1. [PHP](#php) +2. [Node.js](#nodejs) +3. [Python](#python) +4. [Ruby](#ruby) +5. [Найденные баги и исправления](#баги) + +--- + +## PHP + +### Образ +``` +ghcr.io/fission/php-env:latest +``` +Сервер: `ReactPHP` на порту `:8888`, файл `/app/server.php`. + +### Entrypoint +``` +main.php::handler +``` +Формат: `filename::functionName`. Двойное двоеточие — разделитель файла и функции. + +### ⚠️ ГЛАВНЫЙ БАГ (2026-05-19) + +**`return ['statusCode' => 200, 'body' => ...]` — PHP env ИГНОРИРУЕТ return value функции.** + +Почему: server.php создаёт `$response = new Response()` (пустой PSR-7 объект), +вызывает функцию и возвращает этот же объект без чтения результата вызова: + +```php +// server.php — реальный код PHP env: +if (function_exists($userFunction)) { + $response = new Response(); + ob_end_clean(); + $userFunction(['request' =>$request, 'response' => $response, 'logger' => $logger]); + return $response; // ← возвращает оригинальный объект, не результат вызова +} +``` + +### Правильный формат — PSR-7 + +Функция должна **писать** в `$ctx["response"]->getBody()`: + +```php +getBody()->write(json_encode([ + 'status' => 'ok', + 'value' => 42, + ])); +} +``` + +Доступные ключи контекста: +- `$ctx["request"]` — `Psr\Http\Message\ServerRequestInterface` +- `$ctx["response"]` — `Psr\Http\Message\ResponseInterface` (RingCentral\Psr7) +- `$ctx["logger"]` — `Monolog\Logger` + +### Backwards compatibility (echo) + +Если функция `handler` НЕ объявлена, PHP env перехватывает весь вывод через `ob_start()` и возвращает его как тело ответа: + +```php + 'value']); +``` + +Использовать только для самых простых случаев без работы с запросом. + +### Примеры + +**Минимальный (PSR-7):** +```php +getBody()->write("Hello from PHP"); +} +``` + +**С JSON-ответом:** +```php + 1 + 2, 'lang' => 'php']; + $ctx["response"]->getBody()->write(json_encode($result)); +} +``` + +**С чтением тела запроса:** +```php +getBody(), true); + $name = $body['name'] ?? 'World'; + $ctx["response"]->getBody()->write("Hello, $name!"); +} +``` + +**CPU-нагрузка:** +```php +getBody()->write('result:' . fib(28)); +} +``` + +### Что НЕ работает + +```php +// ❌ НЕПРАВИЛЬНО — return игнорируется +function handler(array $ctx): array { + return ['statusCode' => 200, 'body' => 'hello']; +} + +// ❌ НЕПРАВИЛЬНО — echo не работает если функция объявлена +function handler($ctx) { + echo "hello"; // ob_start уже закрыт перед вызовом +} +``` + +--- + +## Node.js + +### Образ +``` +ghcr.io/fission/node-env:latest +``` +Сервер: Express на порту `:8888`, файл `/usr/src/app/server.js`. + +### Entrypoint +``` +main +``` +**Без расширения `.js`.** + +### ⚠️ ГЛАВНЫЙ БАГ (2026-05-19) + +**`functionName = "main.js"` ломает специализацию.** + +Почему: server.js делает `split(".")` для любого functionName содержащего точку: + +```js +// server.js — реальный код node-env: +if (req.body.functionName && req.body.functionName.includes('.')) { + // ПРЕДПОЛАГАЕТСЯ формат: 'file.function' + const entrypoint = req.body.functionName.split("."); + filename = entrypoint[0]; // "main" + funcname = entrypoint[1]; // "js" ← ищет экспорт с именем "js"! +} +``` + +`"main.js"` → `filename="main"`, `funcname="js"` → после загрузки модуля ищет `module["js"]` → undefined → specialize 500. + +**Правильно:** `functionName = "main"` → `funcname = undefined` → берёт `default export` = `module.exports`. + +### Формат кода + +Консоль оборачивает пользовательский код в CJS wrapper. Пользователь пишет: + +```js +// Простая функция, экспортирует async handler: +module.exports = async function(ctx) { + return { status: 200, body: "Hello from Node.js" }; +}; +``` + +Или через именованный экспорт: +```js +async function handler(ctx) { + return { status: 200, body: "ok" }; +} +module.exports = handler; +``` + +Формат возвращаемого объекта: +```js +{ status: 200, body: "строка или JSON" } +``` + +### Примеры + +**Минимальный:** +```js +module.exports = async function(ctx) { + return { status: 200, body: "hello-node" }; +}; +``` + +**С JSON:** +```js +module.exports = async function(ctx) { + const result = { lang: "nodejs", fib25: 75025 }; + return { status: 200, body: JSON.stringify(result) }; +}; +``` + +**CPU-нагрузка:** +```js +function fib(n) { return n < 2 ? n : fib(n-1) + fib(n-2); } +module.exports = async function(ctx) { + return { status: 200, body: "fib35:" + fib(35) }; +}; +``` + +--- + +## Python + +### Образ +``` +naeel/fission-python-env:v1.1 +``` +(НЕ использовать официальный `ghcr.io/fission/python-env` — не поддерживает `def main(event, context)`) + +### Entrypoint +``` +main.main +``` +Формат: `module.function`. + +### Формат функции + +```python +def main(event, context): + return { + "status": 200, + "body": "Hello from Python" + } +``` + +### Примеры + +```python +import json + +def main(event, context): + result = {"lang": "python", "sum": sum(range(100))} + return {"status": 200, "body": json.dumps(result)} +``` + +--- + +## Ruby + +### Образ +``` +ghcr.io/fission/ruby-env:latest +``` + +### Entrypoint +``` +handler +``` +Имя метода в загруженном файле. + +### Формат функции + +```ruby +def handler(event, context) + { statusCode: 200, body: "Hello from Ruby" } +end +``` + +### Примеры + +```ruby +require 'json' + +def handler(event, context) + result = { lang: "ruby", value: (1..10).reduce(:+) } + { statusCode: 200, body: result.to_json } +end +``` + +--- + +## Баги + +### БАГ A — ExpiryReaper race condition (исправлен в v1.3.93) + +**Симптом:** PHP/последний язык в цикле CREATE теряет Package — удаляется ExpiryReaper'ом. + +**Причина:** `activeFunctions` snapshot снимается ДО создания Function. Reaper проверяет orphan packages по устаревшему снимку → последний Package = orphan → удалён. + +**Коммит:** `72e5d50` +**Файл:** `console/internal/cloud/tenant.go` +**Fix:** свежий LIST functions (`freshFunctions`) перед orphan-check вместо кешированного снимка. + +--- + +### БАГ B — Node.js entrypoint `"main.js"` ломает specializeV2 (исправлен в v1.3.94) + +**Симптом:** Node.js функция не специализируется, invoke timeout. + +**Причина:** node-env `specializeV2` делает `split(".")` по любой точке в functionName. `"main.js"` → `funcname="js"` → ищет `module["js"]` → undefined → specialize 500 → бесконечный retry. + +**Коммит:** `090a7af` +**Файл:** `console/internal/runtime/entrypoint.go` +**Fix:** entrypoint Node.js = `"main"` (без расширения). funcname=undefined → берёт `default export`. + +--- + +### БАГ C — PHP `return array` игнорируется (исправлен в test_heavy.sh, v1.3.94) + +**Симптом:** PHP invoke возвращает `status=200` но `response_raw=""`. + +**Причина:** PHP env создаёт пустой `$response` объект, вызывает функцию, возвращает оригинальный объект — return value функции не читается. + +**Коммит:** `042b1e5` +**Файл:** `scripts/test_heavy.sh` +**Fix:** PHP код в тестах переписан на PSR-7: `$ctx["response"]->getBody()->write(...)`. + +--- + +## Сводная таблица: правильный контракт + +| Язык | Entrypoint | Формат функции | return/write | +|--------|---------------------|-------------------------------------------------------|---------------------------| +| Python | `main.main` | `def main(event, context):` | `return {"status":200,...}`| +| Node.js| `main` | `module.exports = async function(ctx) {...}` | `return { status, body }` | +| PHP | `main.php::handler` | `function handler($ctx) {...}` | `$ctx["response"]->getBody()->write(...)` | +| Ruby | `handler` | `def handler(event, context)` | `{ statusCode: 200, body: ... }` | diff --git a/doc/bench-results-pv-2026-05-20.md b/doc/bench-results-pv-2026-05-20.md new file mode 100644 index 0000000..6f2107b --- /dev/null +++ b/doc/bench-results-pv-2026-05-20.md @@ -0,0 +1,40 @@ +# Bench Results — PV Storage — 2026-05-20 + +**RUNID:** 9250986 +**Дата:** Wed May 20 07:23:06–07:27:07 MSK 2026 +**Storage:** PVC (PV) +**API:** https://fission.kube5s.ru/console/api +**SUB:** bench@bench.local +**Итог: PASS=20 FAIL=0 TOTAL=20** + +## Таблица результатов + +| LANG | SIZE | UPLOAD(ms) | COLD(ms) | HOT_AVG(ms) | STATUS | +|--------|---------|-----------|---------|------------|--------| +| python | tiny | 308 | 4384 | 771 | OK | +| python | small | 161 | 1947 | 1860 | OK | +| python | medium | 2751 | 1387 | 1183 | OK | +| python | large | 2764 | 1382 | 1184 | OK | +| nodejs | tiny | 3150 | 8011 | 1175 | OK | +| nodejs | small | 2764 | 1526 | 1198 | OK | +| nodejs | medium | 2745 | 2597 | 1185 | OK | +| nodejs | large | 2737 | 2603 | 1314 | OK | +| ruby | tiny | 3148 | 2105 | 1209 | OK | +| ruby | small | 2768 | 1339 | 1194 | OK | +| ruby | medium | 2759 | **81549** | 1183 | OK (аномалия cold, 3-й прогон подряд) | +| ruby | large | 2743 | 1390 | 787 | OK | +| php | tiny | 1547 | 2546 | 94 | OK | +| php | small | 904 | 1106 | 1213 | OK | +| php | medium | 2744 | 1571 | 1198 | OK ✓ FIXED | +| php | large | 2732 | 1508 | 1151 | OK ✓ FIXED | +| python | archive | 2759 | 1392 | 1178 | OK | +| nodejs | archive | 3177 | 1545 | 1197 | OK | +| ruby | archive | 2780 | 1376 | 1187 | OK | +| php | archive | 2785 | 1416 | 1172 | OK | + +## Что было исправлено + +PHP medium/large — заменены arrow functions `fn()=>` на традиционный синтаксис `function(){}`. +PHP env в кластере < 7.4, arrow functions добавлены в PHP 7.4. + +Лог на VM: `/home/naeel/terra/fission/test-results/bench-func/bench_full_2026-05-20_07-23-06.log` diff --git a/doc/progress.md b/doc/progress.md index dbf21a2..e4f683d 100644 --- a/doc/progress.md +++ b/doc/progress.md @@ -1,5 +1,98 @@ # Progress Log +--- + +## 2026-05-19 (вечер) — Node.js require fix (v1.3.95) + суровые тесты + +### Ветка: `feat/console-pv-storage` (git commit `fe8870a`) + +--- + +### Исправленный баг: Node.js `require` недоступен внутри функции + +**Симптом:** Любая Node.js функция, использующая `require(...)`, падала с: +``` +ERR_MODULE_NOT_FOUND: Cannot find module '/userfunc/deployarchive/main' +``` + +**Реальная причина:** В `console/internal/runtime/nodejs.go` wrapper генерировался как: +```js +(new Function('module', 'exports', code))(__mod, __mod.exports); +``` +`new Function` выполняется в изолированном глобальном scope — `require` там не существует. +Вызов `require('crypto')` внутри кода → `ReferenceError` → ESM fallback → `import('file:///userfunc/deployarchive/main')` без `.js` → ERR_MODULE_NOT_FOUND. + +**Fix** (`console/internal/runtime/nodejs.go`): +```js +// было: +(new Function('module', 'exports', code))(__mod, __mod.exports); +// стало: +(new Function('module', 'exports', 'require', code))(__mod, __mod.exports, require); +``` + +**Также исправлено** (`console/internal/api/package.go`): +Regex `nodeJSWrapperPattern` обновлён — принимает оба формата (старый без `require` и новый с `require`), backward compatible. + +**Unit-тест** (`console/internal/runtime/nodejs_test.go`): +Убраны устаревшие ESM-проверки, добавлены актуальные CJS-проверки. + +**Образ:** `naeel/fission-console:v1.3.95` + +--- + +### Прогоны `test_heavy.sh` + +Два прогона подряд с теми же условиями (namespace `fission-c3511430abe5675b`): + +| Секция | af2f674f | be04cc1b | Вывод | +|--------|---------|---------|-------| +| S1 CREATE (4 яз) | PHP 502 → FAIL | **4/4 PASS** | PHP 502 в первом — transient | +| S1 invoke "не стабилизировался" | FAIL×4 | FAIL×4 | баг теста — WAIT_SEC < cold start | +| S2 invoke "не стабилизировался" | FAIL×4 | FAIL×4 | то же | +| S3 BIG RESPONSE size=46/48 | FAIL×2 | FAIL×2 | баг теста — проверяет поле `raw` (truncated), не реальный ответ | +| S4 invoke "не стабилизировался" | FAIL×4 | FAIL×4 | баг теста — WAIT_SEC < cold start | +| S5 RAPID 12/12 | PASS | PASS | ✓ | +| S6 Python PARALLEL 5/5 | PASS | PASS | ✓ | +| S6 Node.js PARALLEL 5/5 | **2/5 FAIL** | **2/5 FAIL** | pool size=1 не хватает на 5 параллельных — стабильно | +| S6 Ruby PARALLEL 5/5 | PASS | PASS | ✓ | +| S6 PHP PARALLEL 5/5 | 1/5 FAIL | **PASS** | первый — transient | +| S7 Python SLOW(6s) | PASS | PASS | ✓ | +| S7 Node.js SLOW(6s) | PASS (TRY=2) | PASS (TRY=1) | ✓ require fix работает | +| S8 Python MEMORY(20MB) | PASS | PASS | ✓ | +| S8 Node.js MEMORY(20MB) | PASS | PASS | ✓ | +| **ИТОГ** | **PASS=29 FAIL=17** | **PASS=31 FAIL=15** | | + +Логи: `test-results/2026-05-19_*` (два файла) + +--- + +### Анализ стабильных FAIL=15 (оба прогона) + +#### 1. S1+S2+S4 "не стабилизировался" (12 FAIL) +**Причина:** `WAIT_SEC` в `test_heavy.sh` меньше реального cold start. +Функции реально работают — проверено прямыми SSH-запросами через `sleep + curl`. +**Что нужно:** увеличить `WAIT_SEC` или добавить retry-loop с адекватным таймаутом. + +#### 2. S3 BIG RESPONSE (2 FAIL) +**Причина:** тест проверяет `response_raw length` — это длина JSON-поля `raw` из ответа консоли. +Консоль обрезает `raw` до ~46-48 символов. Реальный ответ функции = 50KB (status=200 подтверждено). +**Что нужно:** либо фиксить тест (проверять Content-Length/реальный body), либо фиксить консоль (не обрезать `raw`). + +#### 3. S6 Node.js PARALLEL 2/5 (1 FAIL) +**Причина:** pool size=1 (один pod). При 5 параллельных запросах 2 таймаутятся пока ждут специализацию нового пода. +Python/Ruby справляются 5/5 — у них специализация быстрее. +**Что нужно:** либо увеличить pool size в Node.js environment, либо уменьшить параллельность в тесте до 3. + +--- + +### Что делать дальше с тестами + +- [ ] Исправить `test_heavy.sh` S1/S2/S4: увеличить `WAIT_SEC` до 120-180s или добавить retry-poll +- [ ] Исправить `test_heavy.sh` S3: проверять реальный ответ, не поле `raw` +- [ ] Решить S6 Node.js PARALLEL: увеличить pool или снизить параллельность + +--- + ## 2026-05-19 — PV Storage Migration + Fetcher v1.23.0 ### Ветка: `feat/console-pv-storage` (git commit `2aadae3`, pushed)