# Руководство: контракт функций в 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: ... }` |