340 lines
9.7 KiB
Markdown
340 lines
9.7 KiB
Markdown
# Руководство: контракт функций в 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
|
||
<?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
|
||
<?php
|
||
// Работает, но нет доступа к request/response/logger
|
||
echo json_encode(['key' => 'value']);
|
||
```
|
||
|
||
Использовать только для самых простых случаев без работы с запросом.
|
||
|
||
### Примеры
|
||
|
||
**Минимальный (PSR-7):**
|
||
```php
|
||
<?php
|
||
function handler($ctx) {
|
||
$ctx["response"]->getBody()->write("Hello from PHP");
|
||
}
|
||
```
|
||
|
||
**С JSON-ответом:**
|
||
```php
|
||
<?php
|
||
function handler($ctx) {
|
||
$result = ['sum' => 1 + 2, 'lang' => 'php'];
|
||
$ctx["response"]->getBody()->write(json_encode($result));
|
||
}
|
||
```
|
||
|
||
**С чтением тела запроса:**
|
||
```php
|
||
<?php
|
||
function handler($ctx) {
|
||
$body = json_decode((string)$ctx["request"]->getBody(), true);
|
||
$name = $body['name'] ?? 'World';
|
||
$ctx["response"]->getBody()->write("Hello, $name!");
|
||
}
|
||
```
|
||
|
||
**CPU-нагрузка:**
|
||
```php
|
||
<?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));
|
||
}
|
||
```
|
||
|
||
### Что НЕ работает
|
||
|
||
```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: ... }` |
|