9.7 KiB
Руководство: контракт функций в Fission runtime
Дата: 2026-05-19
Версия консоли: v1.3.94+
Источник: дизассемблирование реальных env-образов в кластере + отладка тестов
Содержание
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 объект),
вызывает функцию и возвращает этот же объект без чтения результата вызова:
// 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
function handler($ctx) {
$response = $ctx["response"];
$response->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
// Работает, но нет доступа к request/response/logger
echo json_encode(['key' => 'value']);
Использовать только для самых простых случаев без работы с запросом.
Примеры
Минимальный (PSR-7):
<?php
function handler($ctx) {
$ctx["response"]->getBody()->write("Hello from PHP");
}
С JSON-ответом:
<?php
function handler($ctx) {
$result = ['sum' => 1 + 2, 'lang' => 'php'];
$ctx["response"]->getBody()->write(json_encode($result));
}
С чтением тела запроса:
<?php
function handler($ctx) {
$body = json_decode((string)$ctx["request"]->getBody(), true);
$name = $body['name'] ?? 'World';
$ctx["response"]->getBody()->write("Hello, $name!");
}
CPU-нагрузка:
<?php
function fib($n) {
if ($n < 2) return $n;
return fib($n-1) + fib($n-2);
}
function handler($ctx) {
$ctx["response"]->getBody()->write('result:' . fib(28));
}
Что НЕ работает
// ❌ НЕПРАВИЛЬНО — 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 содержащего точку:
// 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. Пользователь пишет:
// Простая функция, экспортирует async handler:
module.exports = async function(ctx) {
return { status: 200, body: "Hello from Node.js" };
};
Или через именованный экспорт:
async function handler(ctx) {
return { status: 200, body: "ok" };
}
module.exports = handler;
Формат возвращаемого объекта:
{ status: 200, body: "строка или JSON" }
Примеры
Минимальный:
module.exports = async function(ctx) {
return { status: 200, body: "hello-node" };
};
С JSON:
module.exports = async function(ctx) {
const result = { lang: "nodejs", fib25: 75025 };
return { status: 200, body: JSON.stringify(result) };
};
CPU-нагрузка:
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.
Формат функции
def main(event, context):
return {
"status": 200,
"body": "Hello from 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
Имя метода в загруженном файле.
Формат функции
def handler(event, context)
{ statusCode: 200, body: "Hello from Ruby" }
end
Примеры
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: ... } |