Files
fission-console/doc/RUNTIME_FUNCTION_GUIDE.md
T

9.7 KiB
Raw Blame History

Руководство: контракт функций в Fission runtime

Дата: 2026-05-19
Версия консоли: v1.3.94+
Источник: дизассемблирование реальных env-образов в кластере + отладка тестов


Содержание

  1. PHP
  2. Node.js
  3. Python
  4. 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 объект), вызывает функцию и возвращает этот же объект без чтения результата вызова:

// 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: ... }