Compare commits
17
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3e36ff13a8 | ||
|
|
53ef35968e | ||
|
|
ab128515ab | ||
|
|
296b538d20 | ||
|
|
7539680ae0 | ||
|
|
005b4786c0 | ||
|
|
a2e1723e33 | ||
|
|
dd7e42caf4 | ||
|
|
fe8870af71 | ||
|
|
27dbe3bccb | ||
|
|
090a7afdd7 | ||
|
|
042b1e5046 | ||
|
|
72e5d50cd0 | ||
|
|
fd075a7f80 | ||
|
|
78f1a2cfc4 | ||
|
|
2aadae339e | ||
|
|
3eb4f5669b |
@@ -19,6 +19,7 @@ console/fission-console
|
||||
|
||||
# Secrets / tokens
|
||||
token.txt
|
||||
helm/fission-s3-secret.yaml
|
||||
|
||||
# Test logs
|
||||
test-results/
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
# Образы нашего Fission — актуальные теги
|
||||
|
||||
> Обновлено: 2026-05-21
|
||||
|
||||
## Текущие образы (production)
|
||||
|
||||
| Компонент | Docker-образ | Тег |
|
||||
|---|---|---|
|
||||
| **fission-bundle** (storagesvc, router, executor, buildermgr, kubewatcher, mqtrigger, timer, webhook) | `naeel/fission-bundle` | `v1.23.1` |
|
||||
| **fission-console** (UI) | `naeel/fission-console` | `v1.3.96` |
|
||||
| **Python environment** | `naeel/fission-python-env` | `v1.1` |
|
||||
| **metrics-collector** | `naeel/metrics-collector` | `v0.1.3` |
|
||||
|
||||
## Storagesvc backend
|
||||
|
||||
**Текущий (2026-05-20): S3** — ngcloud msk-1, бакет `sless-functions`.
|
||||
PV-конфиг сохранён в `helm/fission-values-pv.yaml` для быстрого возврата.
|
||||
|
||||
---
|
||||
|
||||
## Где в коде консоли прописаны образы
|
||||
|
||||
### Python environment
|
||||
Файл: `console/internal/model/types.go`
|
||||
```go
|
||||
LangEnvMap["python"] = EnvConfig{Image: "naeel/fission-python-env:v1.1", ...}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Как пересобрать fission-bundle (после правки в fission-src)
|
||||
|
||||
```bash
|
||||
# 1. Внести правки в ~/fission-src/...
|
||||
|
||||
# 2. rsync на ВМ
|
||||
rsync -az -e "ssh -i ~/.ssh/naeel_vm_id_ed25519 -o StrictHostKeyChecking=no -o ConnectTimeout=10" \
|
||||
~/fission-src/ naeel@5.172.178.213:~/terra/fission-src/
|
||||
|
||||
# 3. На ВМ — собрать бинарник
|
||||
ssh -i ~/.ssh/naeel_vm_id_ed25519 naeel@5.172.178.213 '
|
||||
cd ~/terra/fission-src &&
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o _build/linux_amd64/fission-bundle ./cmd/fission-bundle/ &&
|
||||
echo OK'
|
||||
|
||||
# 4. На ВМ — собрать и запушить образ (инкрементировать тег!)
|
||||
ssh -i ~/.ssh/naeel_vm_id_ed25519 naeel@5.172.178.213 '
|
||||
cd ~/terra/fission-src/_build/linux_amd64 &&
|
||||
docker build -t naeel/fission-bundle:vX.Y.Z . &&
|
||||
docker push naeel/fission-bundle:vX.Y.Z'
|
||||
|
||||
# 5. Задеплоить только тот сервис, который менялся
|
||||
ssh -i ~/.ssh/naeel_vm_id_ed25519 naeel@5.172.178.213 '
|
||||
kubectl set image -n fission deploy/storagesvc storagesvc=naeel/fission-bundle:vX.Y.Z &&
|
||||
kubectl rollout status -n fission deploy/storagesvc'
|
||||
```
|
||||
|
||||
### Сервисы и имена контейнеров (для kubectl set image)
|
||||
|
||||
| Deployment | Container name |
|
||||
|---|---|
|
||||
| `deploy/storagesvc` | `storagesvc` |
|
||||
| `deploy/router` | `router` |
|
||||
| `deploy/executor` | `executor` |
|
||||
| `deploy/buildermgr` | `buildermgr` |
|
||||
| `deploy/kubewatcher` | `kubewatcher` |
|
||||
| `deploy/timer` | `timer` |
|
||||
| `deploy/webhook` | `webhook` |
|
||||
| `deploy/mqtrigger-keda` | `mqtrigger-keda` |
|
||||
| `deploy/fission-console` | `fission-console` |
|
||||
|
||||
---
|
||||
|
||||
## Как пересобрать fission-console (после правки в fission-console)
|
||||
|
||||
Подробный порядок — в `CONTRIBUTING.md` консоли.
|
||||
Кратко:
|
||||
1. Увеличить тег в `console/deploy/console.yaml` (vX.Y.Z → vX.Y.Z+1)
|
||||
2. Обновить версию в `console/ui/index.html` (2 места)
|
||||
3. rsync на ВМ
|
||||
4. Проверить файлы на ВМ (grep)
|
||||
5. `docker build -t naeel/fission-console:vX.Y.Z+1`
|
||||
6. `docker push naeel/fission-console:vX.Y.Z+1`
|
||||
7. `kubectl apply -f ~/terra/fission/console/deploy/console.yaml` — **не** rollout restart!
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ ВАЖНО
|
||||
|
||||
- **НИКОГДА** не использовать официальный `ghcr.io/fission/python-env` — не поддерживает `def main(event, context)` → 500
|
||||
- Наш образ `naeel/fission-python-env:v1.1` содержит патч `inspect.signature`
|
||||
- imagePullPolicy: `IfNotPresent` → при смене образа **всегда** менять тег, не переиспользовать старый
|
||||
@@ -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
|
||||
```
|
||||
@@ -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
|
||||
<?php
|
||||
function handler(array $ctx): array {
|
||||
return ['statusCode' => 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
|
||||
<?php
|
||||
function handler($ctx) {
|
||||
$ctx["response"]->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` — полный справочник контрактов.
|
||||
|
||||
|
||||
@@ -54,9 +54,31 @@
|
||||
- Не ломай контракт функции выдуманными обертками или неподходящими типами.
|
||||
|
||||
### PHP
|
||||
- Возвращай корректный PHP-код функции.
|
||||
- Следи за открывающим тегом и синтаксисом массива/строк.
|
||||
- Не выдумывай несуществующие `Fission\\Contract` классы или SDK, если задача про обычную функцию.
|
||||
- **КРИТИЧНО:** PHP env НЕ читает return value функции. `return ['statusCode'=>200,'body'=>...]` — тело ответа будет ПУСТЫМ.
|
||||
- Запись в ответ — ТОЛЬКО через `$ctx["response"]->getBody()->write(...)`:
|
||||
```php
|
||||
<?php
|
||||
function handler($ctx) {
|
||||
$ctx["response"]->getBody()->write(json_encode(['key' => 'value']));
|
||||
}
|
||||
```
|
||||
- Сигнатура: `function handler($ctx)` — без type hint `array` и без `: array` return type.
|
||||
- Доступ к запросу: `$ctx["request"]`, к логгеру: `$ctx["logger"]`.
|
||||
- Открывающий тег `<?php` обязателен.
|
||||
- Не выдумывай несуществующие `Fission\\Contract` классы или SDK.
|
||||
- Альтернатива (только если нет доступа к request): простой `echo` без объявления функции handler:
|
||||
```php
|
||||
<?php
|
||||
echo json_encode(['key' => '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-код функции.
|
||||
|
||||
@@ -102,6 +102,58 @@ Backend сервис для управления serverless-функциями
|
||||
|
||||
## Деплой
|
||||
|
||||
### Хранилище архивов функций (storagesvc)
|
||||
|
||||
Fission хранит ZIP-архивы функций через `storagesvc`. Поддерживается два бэкенда:
|
||||
|
||||
#### S3 (рекомендуется)
|
||||
|
||||
```bash
|
||||
# 1. Создать файл с credentials (НЕ коммитить в git):
|
||||
cat > helm/fission-s3-secret.yaml << 'EOF'
|
||||
persistence:
|
||||
s3:
|
||||
accessKeyId: <ACCESS_KEY>
|
||||
secretAccessKey: <SECRET_KEY>
|
||||
EOF
|
||||
|
||||
# 2. Применить:
|
||||
kubectl delete deploy executor router storagesvc -n fission # убрать field manager конфликты
|
||||
helm upgrade fission fission-charts/fission-all --version 1.22.1 \
|
||||
-n fission \
|
||||
-f helm/fission-values.yaml \
|
||||
-f helm/fission-s3-secret.yaml \
|
||||
--no-hooks --timeout 180s
|
||||
```
|
||||
|
||||
Параметры S3 (bucket, endpoint, region) — в `helm/fission-values.yaml`.
|
||||
Credentials — только в `helm/fission-s3-secret.yaml` (файл в `.gitignore`).
|
||||
|
||||
#### PV (возврат/альтернатива)
|
||||
|
||||
```bash
|
||||
kubectl delete deploy executor router storagesvc -n fission
|
||||
helm upgrade fission fission-charts/fission-all --version 1.22.1 \
|
||||
-n fission \
|
||||
-f helm/fission-values-pv.yaml \
|
||||
--no-hooks --timeout 180s
|
||||
```
|
||||
|
||||
Никаких секретов не нужно. StorageClass `vcd-disk-ext4` (VMware VCD), 2Gi.
|
||||
|
||||
#### Сравнение
|
||||
|
||||
| | S3 | PV |
|
||||
|---|---|---|
|
||||
| Upload скорость | ~1-3s (сеть) | ~0.3-2s (кэш) |
|
||||
| Cold start | ≈ одинаково | ≈ одинаково |
|
||||
| Hot (основная нагрузка) | идентично | идентично |
|
||||
| Масштабирование storagesvc | ✅ N реплик | ❌ ReadWriteOnce |
|
||||
| Переезд кластера | ✅ просто бакет | ❌ надо мигрировать PV |
|
||||
| Backup | ✅ встроен в S3 | ручной |
|
||||
|
||||
---
|
||||
|
||||
| Параметр | Значение |
|
||||
|---|---|
|
||||
| Кластер | `kube5s.ru` |
|
||||
|
||||
@@ -1,20 +1,26 @@
|
||||
MQ-триггер (MessageQueue) — запуск при появлении сообщения в очереди (Kafka, NATS, RabbitMQ и т.д.)
|
||||
Kube-Watch триггер — запуск при изменении объектов в Kubernetes - а это разве может быть реализовано ?
|
||||
# TODO / Статус фич (актуально: 2026-05-21)
|
||||
|
||||
В Fission — да, это штатные встроенные типы триггеров. Fission из коробки поддерживает MQ (Kafka, NATS, Azure Queue) и Kube-Watch через отдельные компоненты (kubewatcher, mqtrigger).
|
||||
✅ = реализовано, ❌ = нет, ⚠️ = частично
|
||||
|
||||
Но в вашей консоли это не реализовано — нет UI для создания/управления этими триггерами. Только HTTP и Cron. Если потребуется — можно добавить.
|
||||
### Триггеры
|
||||
✅ HTTP Trigger — автосоздание при create function
|
||||
✅ Time Trigger (Cron) — CRUD
|
||||
✅ MQ Trigger (SQS) — sqs-consumer Deployment + Secret
|
||||
✅ Kubernetes Watch Trigger — CRUD
|
||||
|
||||
от что реально полезно и не очень сложно:
|
||||
### Функции
|
||||
✅ Клонирование (v1.3.80)
|
||||
✅ Переменные окружения (v1.3.71)
|
||||
✅ Зависимости deps (requirements.txt/Gemfile/composer.json)
|
||||
✅ AI: проверка синтаксиса (реальные линтеры)
|
||||
✅ AI: объяснение архива (LLM)
|
||||
✅ AI: генерация кода (LLM)
|
||||
|
||||
Для пользователя:
|
||||
### Статистика
|
||||
✅ Billing в PostgreSQL
|
||||
✅ Grafana per namespace
|
||||
|
||||
История вызовов — последние N вызовов функции: время, статус, время ответа. Сейчас вызвал и забыл.
|
||||
Метрики — счётчик вызовов, средний latency, % ошибок. У вас уже есть metrics-collector — данные есть, нужен только UI.
|
||||
Автообновление логов — сейчас нужно жать "Обновить" вручную. Можно автополлинг каждые 3-5 сек.
|
||||
Копировать маршрут одной кнопкой — иконка 📋 рядом с маршрутом в таблице.
|
||||
Статус функции — Ready/NotReady/Error прямо в таблице (сейчас не видно).
|
||||
Для управления:
|
||||
6. Клонировать функцию — создать копию с новым именем, сохранив код и настройки.
|
||||
7. Экспорт/импорт — скачать функцию как zip, загрузить обратно.
|
||||
8. Переменные окружения — env vars для функции через UI (сейчас нельзя задать).
|
||||
### Не реализовано
|
||||
❌ Экспорт/импорт функций (zip)
|
||||
❌ Автообновление логов (polling)
|
||||
❌ Уведомления при ошибках
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
@@ -0,0 +1,263 @@
|
||||
#!/usr/bin/env bash
|
||||
# diag_bugs.sh — диагностика двух аномалий из bench_full run=9248232
|
||||
# BUG-1: php/medium + php/large COLD-TIMEOUT (гипотеза: arrow functions PHP 7.4+)
|
||||
# BUG-2: ruby/medium cold=80451ms (гипотеза: poolmgr eviction — проверяем воспроизводимость)
|
||||
|
||||
set -uo pipefail
|
||||
|
||||
SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
|
||||
ROOT_DIR=$(cd "${SCRIPT_DIR}/.." && pwd)
|
||||
source "${ROOT_DIR}/scripts/lib.sh"
|
||||
|
||||
SUB="bench@bench.local"
|
||||
RUNID="diag$(date +%s | tail -c 6)"
|
||||
TMP=$(mktemp -d)
|
||||
|
||||
TIMESTAMP=$(date +%Y-%m-%d_%H-%M-%S)
|
||||
RESULTS_DIR="${ROOT_DIR}/test-results/bench-func"
|
||||
LOGFILE="${RESULTS_DIR}/diag_bugs_${TIMESTAMP}.log"
|
||||
mkdir -p "$RESULTS_DIR"
|
||||
|
||||
declare -a ALL_FNS=()
|
||||
PASS_CNT=0
|
||||
FAIL_CNT=0
|
||||
|
||||
cleanup() {
|
||||
rm -rf "$TMP" 2>/dev/null || true
|
||||
for fn in "${ALL_FNS[@]:-}"; do
|
||||
curl -s -X DELETE "${BASE}/functions/${fn}" \
|
||||
-H "X-Auth-Token: ${SUB}" >/dev/null 2>&1 || true
|
||||
done
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
log() { echo "$@" | tee -a "$LOGFILE"; }
|
||||
escape_code() { python3 -c 'import json,sys; print(json.dumps(sys.stdin.read()))'; }
|
||||
get_status() {
|
||||
python3 -c "import json,sys
|
||||
try: d=json.load(sys.stdin); print(d.get('status',''))
|
||||
except: print('')" 2>/dev/null
|
||||
}
|
||||
get_field() {
|
||||
local f="$1"
|
||||
python3 -c "import json,sys
|
||||
try: d=json.load(sys.stdin); print(d.get('$f',''))
|
||||
except: print('')" 2>/dev/null
|
||||
}
|
||||
|
||||
log "════════════════════════════════════════════════════════════════"
|
||||
log " diag_bugs.sh run=${RUNID} $(date)"
|
||||
log " API: ${BASE} SUB: ${SUB}"
|
||||
log "════════════════════════════════════════════════════════════════"
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════
|
||||
# Вспомогательная: загрузить функцию, сделать cold+hot invoke, вернуть результат
|
||||
# bench_one LABEL LANG CODEFILE [EXPECTED_STR]
|
||||
# ═══════════════════════════════════════════════════════════════════
|
||||
bench_one() {
|
||||
local label="$1" lang="$2" codefile="$3" expected="${4:-}"
|
||||
local fn="bench-${label}-${RUNID}"
|
||||
ALL_FNS+=("$fn")
|
||||
|
||||
# Upload
|
||||
local code_esc t0 t1 R CODE BODY UPLOAD_MS
|
||||
code_esc=$(cat "$codefile" | escape_code)
|
||||
t0=$(date +%s%3N)
|
||||
R=$(curl -s -w "\n%{http_code}" -X POST "${BASE}/functions" \
|
||||
-H "X-Auth-Token: ${SUB}" -H "Content-Type: application/json" \
|
||||
-d "{\"name\":\"${fn}\",\"language\":\"${lang}\",\"code\":${code_esc}}" 2>/dev/null)
|
||||
t1=$(date +%s%3N)
|
||||
CODE=$(echo "$R" | tail -1)
|
||||
BODY=$(echo "$R" | head -n -1)
|
||||
UPLOAD_MS=$((t1-t0))
|
||||
|
||||
if [ "$CODE" != "201" ]; then
|
||||
log " [${label}] UPLOAD FAIL HTTP=${CODE}: $(echo "$BODY" | head -c 200)"
|
||||
FAIL_CNT=$((FAIL_CNT+1))
|
||||
return 1
|
||||
fi
|
||||
log " [${label}] upload OK ${UPLOAD_MS}ms"
|
||||
|
||||
# Cold start (retry 20×5s)
|
||||
local tc0 COLD_MS=0 cold_ok=0 cold_body=""
|
||||
tc0=$(date +%s%3N)
|
||||
for i in $(seq 1 20); do
|
||||
local resp s raw
|
||||
resp=$(curl -s --max-time 35 -X POST "${BASE}/functions/${fn}/invoke" \
|
||||
-H "X-Auth-Token: ${SUB}" -H "Content-Type: application/json" -d '{}' 2>/dev/null)
|
||||
s=$(echo "$resp" | get_status)
|
||||
raw=$(echo "$resp" | get_field "response_raw")
|
||||
if [ "$s" = "200" ]; then
|
||||
if [ -z "$expected" ] || echo "$raw" | grep -qF "$expected"; then
|
||||
local tc1; tc1=$(date +%s%3N)
|
||||
COLD_MS=$((tc1-tc0))
|
||||
cold_ok=1
|
||||
cold_body="$raw"
|
||||
break
|
||||
fi
|
||||
fi
|
||||
sleep 5
|
||||
done
|
||||
|
||||
if [ $cold_ok -eq 0 ]; then
|
||||
log " [${label}] COLD TIMEOUT"
|
||||
FAIL_CNT=$((FAIL_CNT+1))
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Hot ×3
|
||||
local HOT_TOTAL=0 hot_ok=0
|
||||
for i in 1 2 3; do
|
||||
local th0 th1 resp s
|
||||
th0=$(date +%s%3N)
|
||||
resp=$(curl -s --max-time 35 -X POST "${BASE}/functions/${fn}/invoke" \
|
||||
-H "X-Auth-Token: ${SUB}" -H "Content-Type: application/json" -d '{}' 2>/dev/null)
|
||||
th1=$(date +%s%3N)
|
||||
s=$(echo "$resp" | get_status)
|
||||
[ "$s" = "200" ] && { HOT_TOTAL=$((HOT_TOTAL+th1-th0)); hot_ok=$((hot_ok+1)); }
|
||||
done
|
||||
local HOT_AVG=0; [ $hot_ok -gt 0 ] && HOT_AVG=$((HOT_TOTAL/hot_ok))
|
||||
|
||||
log " [${label}] OK cold=${COLD_MS}ms hot_avg=${HOT_AVG}ms (${hot_ok}/3)"
|
||||
[ -n "$cold_body" ] && log " [${label}] body=$(echo "$cold_body" | head -c 120)"
|
||||
PASS_CNT=$((PASS_CNT+1))
|
||||
}
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════
|
||||
# BUG-1: PHP arrow functions
|
||||
# ═══════════════════════════════════════════════════════════════════
|
||||
log ""
|
||||
log "┌─────────────────────────────────────────────────────────────────┐"
|
||||
log "│ BUG-1: PHP arrow functions (fn()=> требует PHP 7.4+) │"
|
||||
log "└─────────────────────────────────────────────────────────────────┘"
|
||||
|
||||
# Тест 1а: минимальный PHP с arrow function — поддерживается?
|
||||
cat > "${TMP}/php_arrow_min.php" << 'EOF'
|
||||
<?php
|
||||
function main($event, $context) {
|
||||
$nums = [1,2,3,4,5];
|
||||
$doubled = array_map(fn($x) => $x * 2, $nums);
|
||||
return "arrow-ok:" . implode(",", $doubled);
|
||||
}
|
||||
EOF
|
||||
log ""
|
||||
log " [php-arrow-min] Тест: arrow function fn(\$x)=>\$x*2 (PHP 7.4+)"
|
||||
bench_one "php-arrow-min" "php" "${TMP}/php_arrow_min.php" "arrow-ok"
|
||||
|
||||
# Тест 1б: PHP version info через phpinfo-like
|
||||
cat > "${TMP}/php_version.php" << 'EOF'
|
||||
<?php
|
||||
function main($event, $context) {
|
||||
return "php-version:" . PHP_VERSION . " php-major:" . PHP_MAJOR_VERSION . "." . PHP_MINOR_VERSION;
|
||||
}
|
||||
EOF
|
||||
log ""
|
||||
log " [php-version] Тест: получить версию PHP runtime"
|
||||
bench_one "php-version" "php" "${TMP}/php_version.php" "php-version"
|
||||
|
||||
# Тест 1в: PHP medium без arrow functions (обычные function)
|
||||
cat > "${TMP}/php_medium_noarrow.php" << 'EOF'
|
||||
<?php
|
||||
function fib($n){$a=0;$b=1;for($i=0;$i<$n;$i++)[$a,$b]=[$b,$a+$b];return $a;}
|
||||
function sieve($n){$s=array_fill(0,$n+1,true);$s[0]=$s[1]=false;for($i=2;$i*$i<=$n;$i++)if($s[$i])for($j=$i*$i;$j<=$n;$j+=$i)$s[$j]=false;return array_keys(array_filter($s));}
|
||||
function sqrtSum($n){$s=0;for($i=1;$i<=$n;$i++)$s+=sqrt($i);return $s;}
|
||||
function bubbleSort($arr){$n=count($arr);for($i=0;$i<$n;$i++)for($j=0;$j<$n-$i-1;$j++)if($arr[$j]>$arr[$j+1]){$t=$arr[$j];$arr[$j]=$arr[$j+1];$arr[$j+1]=$t;}return $arr;}
|
||||
function matMul($a,$b){$ra=count($a);$ca=count($a[0]);$cb=count($b[0]);$c=array_fill(0,$ra,array_fill(0,$cb,0));for($i=0;$i<$ra;$i++)for($j=0;$j<$cb;$j++)for($k=0;$k<$ca;$k++)$c[$i][$j]+=$a[$i][$k]*$b[$k][$j];return $c;}
|
||||
function processWordsNoArrow($words){
|
||||
$out=[];
|
||||
foreach($words as $s){
|
||||
if(strlen($s)>3) $out[]=strrev(strtoupper($s));
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
function statsArrNoArrow($arr){
|
||||
$n=count($arr);$m=array_sum($arr)/$n;
|
||||
$v=0; foreach($arr as $x) $v+=($x-$m)*($x-$m); $v/=$n;
|
||||
return['n'=>$n,'mean'=>round($m,4),'std'=>round(sqrt($v),4)];
|
||||
}
|
||||
|
||||
function main($event,$context){
|
||||
$t0=microtime(true);
|
||||
$words=["alpha","beta","gamma","delta","epsilon","zeta","eta","theta","iota","kappa","lambda","mu"];
|
||||
$f=fib(35);$primes=sieve(500);$s=sqrtSum(1000);
|
||||
$nums=range(50,1);$sorted=bubbleSort($nums);
|
||||
$a=[[1,2,3],[4,5,6]];$b=[[7,8],[9,10],[11,12]];$c=matMul($a,$b);
|
||||
$pw=processWordsNoArrow($words);$st=statsArrNoArrow($primes);
|
||||
$h=substr(hash('sha256','bench-medium-php'),0,16);
|
||||
$elapsed=microtime(true)-$t0;
|
||||
return json_encode(['tag'=>'bench-php-medium-noarrow-ok','fib35'=>$f,'primes'=>count($primes),
|
||||
'sqrt'=>round($s,1),'sorted0'=>$sorted[0],'mat_c00'=>$c[0][0],
|
||||
'words'=>count($pw),'stats'=>$st,'hash'=>$h,'elapsed'=>round($elapsed,5)]);
|
||||
}
|
||||
EOF
|
||||
log ""
|
||||
log " [php-medium-noarrow] PHP medium логика без arrow functions (foreach вместо fn()=>)"
|
||||
bench_one "php-med-noarr" "php" "${TMP}/php_medium_noarrow.php" "bench-php-medium-noarrow-ok"
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════
|
||||
# BUG-2: ruby/medium cold=80451ms — воспроизводимость
|
||||
# ═══════════════════════════════════════════════════════════════════
|
||||
log ""
|
||||
log "┌─────────────────────────────────────────────────────────────────┐"
|
||||
log "│ BUG-2: ruby/medium cold=80451ms — воспроизводимость (×3) │"
|
||||
log "└─────────────────────────────────────────────────────────────────┘"
|
||||
|
||||
cat > "${TMP}/ruby_medium_diag.rb" << 'EOF'
|
||||
require 'json'
|
||||
require 'digest'
|
||||
|
||||
WORDS=%w[alpha beta gamma delta epsilon zeta eta theta iota kappa lambda mu nu xi omicron pi rho sigma tau upsilon phi chi psi omega]
|
||||
|
||||
def fib(n); a,b=0,1; n.times{a,b=b,a+b}; a; end
|
||||
def sieve(n)
|
||||
s=Array.new(n+1,true); s[0]=s[1]=false
|
||||
(2..Math.sqrt(n).to_i).each{|i|(i*i..n).step(i){|j|s[j]=false} if s[i]}
|
||||
(0..n).select{|i|s[i]}
|
||||
end
|
||||
def sqrt_sum(n); (1..n).sum{|i|Math.sqrt(i)}; end
|
||||
def bubble_sort(arr)
|
||||
arr=arr.dup; n=arr.length
|
||||
n.times{(n-1).times{|j|arr[j],arr[j+1]=arr[j+1],arr[j] if arr[j]>arr[j+1]}}
|
||||
arr
|
||||
end
|
||||
def mat_mul(a,b)
|
||||
ra,ca,cb=a.length,a[0].length,b[0].length
|
||||
Array.new(ra){|i|Array.new(cb){|j|(0...ca).sum{|k|a[i][k]*b[k][j]}}}
|
||||
end
|
||||
def process_words(w); w.select{|s|s.length>3}.map{|s|s.upcase.reverse}; end
|
||||
def get_stats(arr)
|
||||
n=arr.length; m=arr.sum.to_f/n; v=arr.sum{|x|(x-m)**2}/n
|
||||
{n:n, mean:m.round(4), std:Math.sqrt(v).round(4)}
|
||||
end
|
||||
|
||||
def handler(ctx)
|
||||
t0=Time.now
|
||||
f=fib(35); primes=sieve(500); s=sqrt_sum(1000)
|
||||
nums=(1..50).to_a.reverse; sorted=bubble_sort(nums)
|
||||
a=[[1,2,3],[4,5,6]]; b=[[7,8],[9,10],[11,12]]; c=mat_mul(a,b)
|
||||
pw=process_words(WORDS); st=get_stats(primes)
|
||||
h=Digest::SHA256.hexdigest('bench-medium-ruby')[0,16]
|
||||
elapsed=Time.now-t0
|
||||
{ statusCode: 200, body: JSON.generate({tag:"bench-ruby-medium-ok", fib35:f, primes:primes.length,
|
||||
sqrt:s.round(1), sorted0:sorted[0], mat_c00:c[0][0],
|
||||
words:pw.length, stats:st, hash:h, elapsed:elapsed.round(5)}) }
|
||||
end
|
||||
EOF
|
||||
|
||||
log ""
|
||||
log " ruby/medium × 3 независимых функции (разные имена → чистый cold start каждый раз)"
|
||||
for attempt in 1 2 3; do
|
||||
log ""
|
||||
log " --- Попытка ${attempt}/3 ---"
|
||||
bench_one "ruby-med-${attempt}" "ruby" "${TMP}/ruby_medium_diag.rb" "bench-ruby-medium-ok"
|
||||
done
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════
|
||||
# ИТОГ
|
||||
# ═══════════════════════════════════════════════════════════════════
|
||||
log ""
|
||||
log "════════════════════════════════════════════════════════════════"
|
||||
log " ИТОГ: PASS=${PASS_CNT} FAIL=${FAIL_CNT} TOTAL=$((PASS_CNT+FAIL_CNT))"
|
||||
log " Лог: ${LOGFILE}"
|
||||
log " Готово: $(date)"
|
||||
log "════════════════════════════════════════════════════════════════"
|
||||
@@ -0,0 +1,5 @@
|
||||
def main(event, context):
|
||||
return {
|
||||
"statusCode": 200,
|
||||
"body": "hello, world!"
|
||||
}
|
||||
@@ -58,7 +58,7 @@ spec:
|
||||
serviceAccountName: fission-console
|
||||
containers:
|
||||
- name: console
|
||||
image: naeel/fission-console:v1.3.92
|
||||
image: naeel/fission-console:v1.3.96
|
||||
imagePullPolicy: Always
|
||||
ports:
|
||||
- containerPort: 8090
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"fission-console/internal/billing"
|
||||
"fission-console/internal/fission"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
@@ -48,9 +49,10 @@ func newCloneTestServer(t *testing.T, storagesvcHandler http.HandlerFunc, objs .
|
||||
}
|
||||
|
||||
s := &Server{
|
||||
dyn: dynClient,
|
||||
ns: "fission-test",
|
||||
http: httpClient,
|
||||
dyn: dynClient,
|
||||
ns: "fission-test",
|
||||
http: httpClient,
|
||||
billing: billing.NoopStore{},
|
||||
}
|
||||
return s, storagesvc
|
||||
}
|
||||
|
||||
@@ -113,7 +113,7 @@ func decodeArchiveBytesToSource(decoded []byte) (string, error) {
|
||||
return "", io.ErrUnexpectedEOF
|
||||
}
|
||||
|
||||
var nodeJSWrapperPattern = regexp.MustCompile(`(?s)^const __mod = \{ exports: \{\} \};\s*\(new Function\('module', 'exports', (.+?)\)\)\(__mod, __mod\.exports\);\s*const _fn = __mod\.exports;`)
|
||||
var nodeJSWrapperPattern = regexp.MustCompile(`(?s)^const __mod = \{ exports: \{\} \};\s*\(new Function\('module', 'exports', (?:'require', )?(.+?)\)\)\(__mod, __mod\.exports(?:, require)?\);\s*const _fn = __mod\.exports;`)
|
||||
|
||||
func decodeNodeJSWrapperSource(decoded []byte) (string, bool) {
|
||||
text := string(decoded)
|
||||
|
||||
@@ -393,7 +393,17 @@ func (m *NSManager) reapExpiredFunctionsInNS(ctx context.Context, ns string, now
|
||||
}
|
||||
}
|
||||
|
||||
// Orphan packages — пакеты без соответствующей функции
|
||||
// Orphan packages — пакеты без соответствующей функции.
|
||||
// Перечитываем Functions заново (fresh LIST) чтобы избежать race condition:
|
||||
// новая функция может быть создана между снятием activeFunctions и этой проверкой.
|
||||
freshFunctions, freshErr := m.dyn.Resource(fission.FunctionGVR).Namespace(ns).List(ctx, metav1.ListOptions{})
|
||||
if freshErr != nil {
|
||||
return
|
||||
}
|
||||
freshFunctionSet := make(map[string]struct{}, len(freshFunctions.Items))
|
||||
for _, fn := range freshFunctions.Items {
|
||||
freshFunctionSet[fn.GetName()] = struct{}{}
|
||||
}
|
||||
packages, pkgListErr := m.dyn.Resource(fission.PackageGVR).Namespace(ns).List(ctx, metav1.ListOptions{})
|
||||
if pkgListErr == nil {
|
||||
for _, pkg := range packages.Items {
|
||||
@@ -401,8 +411,13 @@ func (m *NSManager) reapExpiredFunctionsInNS(ctx context.Context, ns string, now
|
||||
if !strings.HasSuffix(pkgName, "-pkg") {
|
||||
continue
|
||||
}
|
||||
// Пропускаем свежесозданные пакеты: Function CRD может ещё не успеть
|
||||
// создаться после загрузки Package (race condition при быстром ExpiryReaper).
|
||||
if age := time.Since(pkg.GetCreationTimestamp().Time); age < 2*time.Minute {
|
||||
continue
|
||||
}
|
||||
fnName := strings.TrimSuffix(pkgName, "-pkg")
|
||||
if _, exists := activeFunctions[fnName]; !exists {
|
||||
if _, exists := freshFunctionSet[fnName]; !exists {
|
||||
log.Printf("cloud.ExpiryReaper: deleting orphan package %s/%s", ns, pkgName)
|
||||
_ = m.dyn.Resource(fission.PackageGVR).Namespace(ns).Delete(ctx, pkgName, metav1.DeleteOptions{})
|
||||
}
|
||||
|
||||
@@ -11,6 +11,9 @@ package runtime
|
||||
func DefaultEntrypoint(lang string) string {
|
||||
switch lang {
|
||||
case "nodejs":
|
||||
// "main" (без .js): node-env.specializeV2 делает split(".") для functionName
|
||||
// с точкой → funcname="js" → ищет экспорт "js" → 500.
|
||||
// Без расширения: funcname=undefined → берёт default export (module.exports = __invoke).
|
||||
return "main"
|
||||
case "php":
|
||||
// Fission php-env: filename::functionName
|
||||
|
||||
@@ -23,10 +23,12 @@ func BuildJSDeployZip(code string) ([]byte, error) {
|
||||
|
||||
// main.js — CommonJS wrapper:
|
||||
// 1. new Function создаёт функцию в пустом модульном контексте (нет import/export)
|
||||
// 2. Передаём ей module и exports как параметры → пользовательский CJS код работает
|
||||
// 3. Экспортируем main/handler/default через module.exports
|
||||
// 2. Передаём ей module, exports и require как параметры → пользовательский CJS код работает
|
||||
// 3. require передаётся явно, т.к. new Function выполняется в глобальном scope
|
||||
// и не имеет доступа к module-local require из CJS контекста main.js
|
||||
// 4. Экспортируем main/handler/default через module.exports
|
||||
wrapper := fmt.Sprintf(`const __mod = { exports: {} };
|
||||
(new Function('module', 'exports', %s))(__mod, __mod.exports);
|
||||
(new Function('module', 'exports', 'require', %s))(__mod, __mod.exports, require);
|
||||
const _fn = __mod.exports;
|
||||
|
||||
async function __invoke(ctx) {
|
||||
|
||||
@@ -33,18 +33,20 @@ func TestBuildJSDeployZipExportsMainAndHandler(t *testing.T) {
|
||||
files[file.Name] = string(content)
|
||||
}
|
||||
|
||||
if files["package.json"] != `{"type":"module"}` {
|
||||
t.Fatalf("package.json = %q, want ESM marker", files["package.json"])
|
||||
}
|
||||
|
||||
mainJS := files["main.js"]
|
||||
if !strings.Contains(mainJS, `export { __invoke as main, __invoke as handler };`) {
|
||||
t.Fatalf("main.js does not export both main and handler: %s", mainJS)
|
||||
if mainJS == "" {
|
||||
t.Fatalf("main.js not found in zip, files: %v", files)
|
||||
}
|
||||
if !strings.Contains(mainJS, `export default __invoke;`) {
|
||||
t.Fatalf("main.js does not export default invoke: %s", mainJS)
|
||||
if !strings.Contains(mainJS, `new Function('module', 'exports', 'require',`) {
|
||||
t.Fatalf("main.js does not pass require to new Function: %s", mainJS)
|
||||
}
|
||||
if !strings.Contains(mainJS, `__mod.exports, require)`) {
|
||||
t.Fatalf("main.js does not forward require: %s", mainJS)
|
||||
}
|
||||
if !strings.Contains(mainJS, `module.exports = __invoke`) {
|
||||
t.Fatalf("main.js does not export __invoke via module.exports: %s", mainJS)
|
||||
}
|
||||
if !strings.Contains(mainJS, `_fn.default || _fn.handler || _fn.main`) {
|
||||
t.Fatalf("main.js lost user export resolution: %s", mainJS)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,7 +103,7 @@
|
||||
<div class="nubes">NUBES</div>
|
||||
<div class="product">FISSION CONSOLE</div>
|
||||
</div>
|
||||
<div style="font-size:0.65rem; color:var(--text-secondary); margin-left:10px; align-self:center; opacity:0.7;">v1.3.92</div>
|
||||
<div style="font-size:0.65rem; color:var(--text-secondary); margin-left:10px; align-self:center; opacity:0.7;">v1.3.96</div>
|
||||
</div>
|
||||
<div class="row" style="margin:0;">
|
||||
<button class="btn ghost" onclick="reloadAll()">Refresh</button>
|
||||
@@ -626,7 +626,7 @@ psycopg2-binary" style="width:100%; box-sizing:border-box; font-family:monospace
|
||||
</div>
|
||||
|
||||
<div class="actions" style="justify-content:space-between; align-items:center;">
|
||||
<span style="font-size:0.75rem; color:var(--text-secondary);">v1.3.92</span>
|
||||
<span style="font-size:0.75rem; color:var(--text-secondary);">v1.3.96</span>
|
||||
<button class="btn ghost" onclick="closeHelp()">Закрыть</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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
|
||||
<?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: ... }` |
|
||||
@@ -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`
|
||||
@@ -0,0 +1,89 @@
|
||||
# Bench Results — S3 Storage (ngcloud msk-1) — 2026-05-20
|
||||
|
||||
**RUNID:** 9251970
|
||||
**Дата:** Wed May 20 07:39:30–07:43:27 MSK 2026
|
||||
**Storage:** S3 (`sless-functions`, `s3.msk-1.ngcloud.ru`, region `msk-1`)
|
||||
**API:** https://fission.kube5s.ru/console/api
|
||||
**SUB:** bench@bench.local
|
||||
**Итог: PASS=20 FAIL=0 TOTAL=20**
|
||||
|
||||
## Таблица результатов S3
|
||||
|
||||
| LANG | SIZE | UPLOAD(ms) | COLD(ms) | HOT_AVG(ms) | STATUS |
|
||||
|--------|---------|-----------|---------|------------|--------|
|
||||
| python | tiny | 3115 | 2453 | 621 | OK |
|
||||
| python | small | 1360 | 889 | 481 | OK |
|
||||
| python | medium | 1370 | 982 | 446 | OK |
|
||||
| python | large | 2561 | 1471 | 1157 | OK |
|
||||
| nodejs | tiny | 3348 | 8347 | 1210 | OK |
|
||||
| nodejs | small | 2907 | 1528 | 1270 | OK |
|
||||
| nodejs | medium | 2949 | 1576 | 1187 | OK |
|
||||
| nodejs | large | 2747 | 2705 | 1146 | OK |
|
||||
| ruby | tiny | 3145 | 2164 | 1190 | OK |
|
||||
| ruby | small | 2959 | 1886 | 1151 | OK |
|
||||
| ruby | medium | 2756 | **80568** | 120 | OK (аномалия, 4-й прогон подряд) |
|
||||
| ruby | large | 1386 | 1652 | 1168 | OK |
|
||||
| php | tiny | 3158 | 1845 | 1156 | OK |
|
||||
| php | small | 2751 | 1565 | 1218 | OK |
|
||||
| php | medium | 2676 | 1516 | 1143 | OK |
|
||||
| php | large | 3146 | 1581 | 1190 | OK |
|
||||
| python | archive | 2752 | 1463 | 1160 | OK |
|
||||
| nodejs | archive | 3191 | 1832 | 1166 | OK |
|
||||
| ruby | archive | 2771 | 1454 | 1297 | OK |
|
||||
| php | archive | 2768 | 1589 | 1188 | OK |
|
||||
|
||||
Лог на VM: `/home/naeel/terra/fission/test-results/bench-func/bench_full_2026-05-20_07-39-30.log`
|
||||
|
||||
## Сравнение PV vs S3
|
||||
|
||||
PV RUNID=9250986 (2026-05-20 07:23) vs S3 RUNID=9251970 (2026-05-20 07:39).
|
||||
|
||||
| LANG | SIZE | UPLOAD PV | UPLOAD S3 | COLD PV | COLD S3 | HOT PV | HOT S3 |
|
||||
|--------|---------|-----------|-----------|---------|---------|--------|--------|
|
||||
| python | tiny | 308 | 3115 | 4384 | 2453 | 771 | 621 |
|
||||
| python | small | 161 | 1360 | 1947 | 889 | 1860 | 481 |
|
||||
| python | medium | 2751 | 1370 | 1387 | 982 | 1183 | 446 |
|
||||
| python | large | 2764 | 2561 | 1382 | 1471 | 1184 | 1157 |
|
||||
| nodejs | tiny | 3150 | 3348 | 8011 | 8347 | 1175 | 1210 |
|
||||
| nodejs | small | 2764 | 2907 | 1526 | 1528 | 1198 | 1270 |
|
||||
| nodejs | medium | 2745 | 2949 | 2597 | 1576 | 1185 | 1187 |
|
||||
| nodejs | large | 2737 | 2747 | 2603 | 2705 | 1314 | 1146 |
|
||||
| ruby | tiny | 3148 | 3145 | 2105 | 2164 | 1209 | 1190 |
|
||||
| ruby | small | 2768 | 2959 | 1339 | 1886 | 1194 | 1151 |
|
||||
| ruby | medium | 2759 | 2756 | 81549 | 80568 | 1183 | 120 |
|
||||
| ruby | large | 2743 | 1386 | 1390 | 1652 | 787 | 1168 |
|
||||
| php | tiny | 1547 | 3158 | 2546 | 1845 | 94 | 1156 |
|
||||
| php | small | 904 | 2751 | 1106 | 1565 | 1213 | 1218 |
|
||||
| php | medium | 2744 | 2676 | 1571 | 1516 | 1198 | 1143 |
|
||||
| php | large | 2732 | 3146 | 1508 | 1581 | 1314 | 1190 |
|
||||
| python | archive | 2759 | 2752 | 1392 | 1463 | 1178 | 1160 |
|
||||
| nodejs | archive | 3177 | 3191 | 1545 | 1832 | 1197 | 1166 |
|
||||
| ruby | archive | 2780 | 2771 | 1376 | 1454 | 1297 | 1297 |
|
||||
| php | archive | 2785 | 2768 | 1416 | 1589 | 1172 | 1188 |
|
||||
|
||||
## Выводы
|
||||
|
||||
### Upload (загрузка архива в storagesvc)
|
||||
- Мелкие файлы (tiny/small): S3 **медленнее** — python/tiny: 308→3115ms (+2807ms), python/small: 161→1360ms (+1199ms)
|
||||
- Средние/крупные файлы: сопоставимо (±200ms)
|
||||
- Причина: HTTP round-trip до S3 ngcloud vs локальный PVC (vcd-disk-ext4 тоже сетевой, но внутри кластера)
|
||||
|
||||
### Cold start (первый invoke)
|
||||
- Python: S3 **быстрее** — small: 1947→889ms, medium: 1387→982ms
|
||||
- Nodejs medium: S3 быстрее — 2597→1576ms
|
||||
- PHP: сопоставимо (±50ms)
|
||||
- Ruby small: S3 медленнее — 1339→1886ms
|
||||
- Итог: cold start на S3 ≈ на PV, разброс в пределах погрешности кластера
|
||||
|
||||
### Hot avg (последующие invoke)
|
||||
- Практически одинаково на обоих бэкендах (±100ms)
|
||||
- Фетчер кэширует архив в pod — хранилище не влияет на hot path
|
||||
|
||||
### ruby/medium аномалия
|
||||
- PV: 81549ms → S3: 80568ms — воспроизводится **4-й раз подряд** на разных бэкендах
|
||||
- Подтверждает: аномалия не связана с типом хранилища, это поведение executor/poolmgr
|
||||
|
||||
### Вердикт
|
||||
**S3 и PV практически эквивалентны по производительности** при нагрузке нашего масштаба.
|
||||
Upload мелких файлов чуть медленнее на S3, cold start — сопоставим.
|
||||
S3 предпочтительнее из-за простоты операций (нет PVC, нет проблем с PV provisioner).
|
||||
+131
@@ -1,5 +1,136 @@
|
||||
# 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)
|
||||
|
||||
**Задача:** Перевести storagesvc с S3 на PV (PersistentVolume), привести все образы к v1.23.0.
|
||||
|
||||
#### Что сделано
|
||||
|
||||
- `helm/fission-values.yaml` — `imageTag: v1.23.0`, `fetcher.image: naeel/fetcher`, `fetcher.imageTag: v1.23.0`, PV backend включён
|
||||
- `naeel/fetcher:v1.23.0` — собран из `fission-src/cmd/fetcher/`, запушен в Docker Hub (отдельный бинарник, в `naeel/fission-bundle` не входит)
|
||||
- `fission-storage-pvc` — Bound, 2Gi, `vcd-disk-ext4`, namespace `fission`; S3 полностью отключён
|
||||
|
||||
#### Баги найдены и задокументированы (`doc/report-2026-05-19-pv-migration-bugs.md`)
|
||||
|
||||
| # | Описание | Причина | Fix |
|
||||
|---|----------|---------|-----|
|
||||
| 1 | Helm upgrade откатил образы с v1.23.0 на v1.22.1 | `imageTag: v1.22.1` в values | Исправлено на v1.23.0 |
|
||||
| 2 | Field manager конфликт при helm upgrade | fission-console патчит FISSION_RESOURCE_NAMESPACES | Перед upgrade: `kubectl delete deploy executor router -n fission` |
|
||||
| 3 | Pool pods ImagePullBackOff — `FETCHER_IMAGE=fission/fetcher:v1.22.0` (не существует) | fetcher — отдельный образ, его нет в fission-bundle | Собрали `naeel/fetcher:v1.23.0` |
|
||||
| 4 | Router JWT password сменился после helm upgrade | Новый secret | Console читает динамически — работает; CLI токены устарели |
|
||||
|
||||
#### Тест-прогон `run_all.sh` — PASS=8 FAIL=0 (319 сек)
|
||||
|
||||
| Скрипт | Результат | Кол-во проверок |
|
||||
|--------|-----------|----------------|
|
||||
| test_auth.sh | ✅ PASS | 14 |
|
||||
| test_functions_crud.sh | ✅ PASS | 29 |
|
||||
| test_ttl.sh | ✅ PASS | 10 |
|
||||
| test_isolation.sh | ✅ PASS | 11 |
|
||||
| test_update.sh | ✅ PASS | 10 |
|
||||
| test_invoke.sh | ✅ PASS | 9 |
|
||||
| test_linters.sh | ✅ PASS | 31 |
|
||||
| test_stress.sh | ✅ PASS | 3 (8 параллельных пользователей) |
|
||||
|
||||
Лог: `test-results/2026-05-19_08-51-31.log`
|
||||
|
||||
---
|
||||
|
||||
## TODO (backlog)
|
||||
|
||||
### Биллинг / статистика для бухгалтерии
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
# Отчёт: миграция S3→PV + баги helm upgrade — 2026-05-19
|
||||
|
||||
## Что делали
|
||||
|
||||
1. Бенчмарк 3 хранилищ (local-path PV, vcd-disk-ext4 PV, S3)
|
||||
2. Переехали storagesvc с S3 на PV (vcd-disk-ext4, 2Gi)
|
||||
3. Обнаружили что S3 фактически не использовался (все Package URL — http://storagesvc..., не s3://)
|
||||
4. Миграция была no-op: перенесено=0, пропущено=44
|
||||
|
||||
---
|
||||
|
||||
## Баги и ошибки
|
||||
|
||||
### БАГ 1: helm upgrade откатил executor/router с v1.23.0 → v1.22.1
|
||||
|
||||
**Причина:** в `helm/fission-values.yaml` был прописан `imageTag: v1.22.1` вместо `v1.23.0`.
|
||||
До helm upgrade executor/router жили вне helm (задеплоены через `kubectl set image`).
|
||||
|
||||
**Симптом:** консоль не показывала функции.
|
||||
|
||||
**Исправление:** изменён `imageTag: v1.23.0` в values, пересоздан executor/router.
|
||||
|
||||
---
|
||||
|
||||
### БАГ 2: field manager конфликт — fission-console vs helm
|
||||
|
||||
**Причина:** fission-console патчит `FISSION_RESOURCE_NAMESPACES` в executor и router через server-side apply.
|
||||
Когда helm пытается обновить те же deployment-ы — конфликт field manager.
|
||||
|
||||
**Симптом:**
|
||||
```
|
||||
conflict with "fission-console" using apps/v1:
|
||||
.spec.template.spec.containers[name="executor"].env[name="FISSION_RESOURCE_NAMESPACES"].value
|
||||
```
|
||||
|
||||
**Обход:** перед `helm upgrade` удалять executor и router вручную:
|
||||
```bash
|
||||
kubectl delete deploy executor router -n fission
|
||||
helm upgrade fission fission-charts/fission-all --version 1.22.1 \
|
||||
-n fission -f helm/fission-values.yaml --no-hooks --timeout 180s
|
||||
```
|
||||
|
||||
**Системное решение:** нужно чтобы fission-console не трогал deployments executor/router напрямую (FISSION_RESOURCE_NAMESPACES должен читаться из ConfigMap или CRD, а не инжектироваться в deployment env).
|
||||
|
||||
---
|
||||
|
||||
### БАГ 3: FETCHER_IMAGE = fission/fetcher:v1.22.0 — образ не существует
|
||||
|
||||
**Причина:** helm chart `fission-all:1.22.1` имеет дефолтное значение:
|
||||
```yaml
|
||||
fetcher:
|
||||
image: fission/fetcher
|
||||
imageTag: v1.22.0
|
||||
```
|
||||
Этот образ на Docker Hub не существует (fission переехал на ghcr.io).
|
||||
|
||||
До наших helm upgrade executor не управлялся helm — FETCHER_IMAGE либо не был задан (код падал на дефолт `ghcr.io/fission/fetcher`), либо был задан правильно вручную.
|
||||
|
||||
После helm upgrade — chart прописал неправильный FETCHER_IMAGE.
|
||||
|
||||
**Симптом:**
|
||||
- Pool pods: `ImagePullBackOff` на контейнере `fetcher`
|
||||
- `fission/fetcher:v1.22.0: not found` на Docker Hub
|
||||
- Функции не запускаются (504 при invoke)
|
||||
|
||||
**Правильное значение:** `naeel/fission-bundle:v1.23.0`
|
||||
Fetcher — часть fission-bundle, отдельного образа нет.
|
||||
Находится в: `fission-src/pkg/fetcher/`
|
||||
|
||||
**Неправильные ключи в values (игнорируются чартом):**
|
||||
```yaml
|
||||
fetcherImage: naeel/fission-bundle # ← НЕПРАВИЛЬНО, такого ключа нет
|
||||
fetcherImageTag: v1.23.0 # ← НЕПРАВИЛЬНО
|
||||
```
|
||||
|
||||
**Правильные ключи:**
|
||||
```yaml
|
||||
fetcher:
|
||||
image: naeel/fission-bundle # ← ПРАВИЛЬНО
|
||||
imageTag: v1.23.0 # ← ПРАВИЛЬНО
|
||||
```
|
||||
|
||||
Шаблон чарта (`charts/fission-all/templates/_helpers.tpl`):
|
||||
```
|
||||
{{- define "fetcherImage" -}}
|
||||
{{- $args := list (.Values.fetcher.repository | default .Values.repository) .Values.fetcher.image .Values.fetcher.imageTag -}}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### БАГ 4: router JWT пароль сменился при helm upgrade
|
||||
|
||||
**Причина:** helm upgrade пересоздал secret `router` с новым паролем.
|
||||
|
||||
| До | После |
|
||||
|----|-------|
|
||||
| `slxGj3G3FAD7l5ms2tJ9` | `58oKMuBIvmBBXHXqaLHw` |
|
||||
|
||||
fission-console читает пароль из secret динамически (`valueFrom: secretKeyRef`) — поэтому сам работает.
|
||||
CLI-инструменты с сохранённым токеном требуют перегенерации токена.
|
||||
|
||||
---
|
||||
|
||||
## Текущее состояние (на момент отчёта)
|
||||
|
||||
| Компонент | Образ | Статус |
|
||||
|-----------|-------|--------|
|
||||
| executor, router, buildermgr, storagesvc, timer, webhook, kubewatcher | `naeel/fission-bundle:v1.23.0` | ✅ Running |
|
||||
| fission-console | `naeel/fission-console:v1.3.92` | ✅ Running |
|
||||
| FETCHER_IMAGE (env в executor) | `fission/fetcher:v1.22.0` | ❌ НЕПРАВИЛЬНО |
|
||||
| fission-storage-pvc | 2Gi, vcd-disk-ext4, Bound | ✅ OK |
|
||||
|
||||
## Что нужно сделать
|
||||
|
||||
1. Исправить `helm/fission-values.yaml` — заменить `fetcherImage`/`fetcherImageTag` на правильную секцию `fetcher:`
|
||||
2. Сделать helm upgrade (с предварительным удалением executor/router)
|
||||
3. Проверить что pool pods поднимаются без ImagePullBackOff
|
||||
4. Проверить что функции nubes1 (`a1`, `c1`) запускаются
|
||||
@@ -0,0 +1,161 @@
|
||||
# Анализ бенчмарка bench_full.sh — 2026-05-20
|
||||
|
||||
## Контекст
|
||||
|
||||
Запущен полный бенчмарк `bench-func/bench_full.sh` (RUNID=9248232).
|
||||
Namespace: `fission-08f114ea3595cccf` (sub: bench@bench.local).
|
||||
4 языка × 4 размера (tiny/small/medium/large) + 4 архивных теста = 20 итого.
|
||||
|
||||
---
|
||||
|
||||
## Результаты bench_full.sh
|
||||
|
||||
| LANG | SIZE | UPLOAD(ms) | COLD(ms) | HOT_AVG(ms) | STATUS |
|
||||
|--------|---------|-----------|---------|------------|----------------|
|
||||
| python | tiny | 2529 | 3317 | 1198 | OK |
|
||||
| python | small | 2764 | 1387 | 1182 | OK |
|
||||
| python | medium | 2762 | 1374 | 1185 | OK |
|
||||
| python | large | 2769 | 1366 | 1653 | OK |
|
||||
| nodejs | tiny | 3158 | 9410 | 1172 | OK (аномалия*) |
|
||||
| nodejs | small | 2751 | 1559 | 1193 | OK |
|
||||
| nodejs | medium | 2754 | 2471 | 1158 | OK |
|
||||
| nodejs | large | 2745 | 1583 | 1190 | OK |
|
||||
| ruby | tiny | 3142 | 3255 | 1162 | OK |
|
||||
| ruby | small | 2737 | 1402 | 1181 | OK |
|
||||
| ruby | medium | 3151 | **80451** | 115 | OK (аномалия!) |
|
||||
| ruby | large | 1116 | 2448 | 104 | OK |
|
||||
| php | tiny | 1137 | 2528 | 1200 | OK |
|
||||
| php | small | 2747 | 1469 | 1158 | OK |
|
||||
| php | medium | 2749 | — | — | **COLD-TIMEOUT** |
|
||||
| php | large | 2725 | — | — | **COLD-TIMEOUT** |
|
||||
| python | archive | 2749 | 1403 | 1176 | OK |
|
||||
| nodejs | archive | 2777 | 1539 | 535 | OK |
|
||||
| ruby | archive | 1367 | 821 | 787 | OK |
|
||||
| php | archive | 2744 | 1575 | 1189 | OK |
|
||||
|
||||
**ИТОГ: PASS=18, FAIL=2**
|
||||
|
||||
*nodejs/tiny cold=9410ms — одиночный выброс, не воспроизводился.
|
||||
|
||||
---
|
||||
|
||||
## Аномалия: ruby/medium cold=80451ms
|
||||
|
||||
### Анализ
|
||||
Проведено ×6 независимых прогонов (diag_bugs.sh, два запуска):
|
||||
- 3780 / 1402 / 1384 / 3225 / 1435 / 1773 ms
|
||||
|
||||
**Вывод:** не воспроизводится. Разовый выброс (вероятно node pressure или transient network).
|
||||
**Статус: ЗАКРЫТ. Не баг.**
|
||||
|
||||
---
|
||||
|
||||
## Баг: PHP medium + large COLD-TIMEOUT
|
||||
|
||||
### Первичная гипотеза (верна)
|
||||
`bench_full.sh` генерирует PHP код для medium/large с arrow functions (PHP 7.4+):
|
||||
```php
|
||||
// medium
|
||||
array_map(fn($s) => strrev(strtoupper($s)), $words)
|
||||
array_map(fn($x) => ($x - $m) ** 2, $data)
|
||||
|
||||
// large
|
||||
array_map(fn($i) => array_map(fn($j) => ..., ...), ...)
|
||||
```
|
||||
PHP env на кластере — версия **< 7.4**. Arrow syntax → Parse error → pod возвращает 500.
|
||||
|
||||
PHP tiny/small и PHP archive — используют традиционный синтаксис без `fn()=>` → OK.
|
||||
|
||||
### Механизм pool exhaustion (важно!)
|
||||
|
||||
При invoke сломанной функции:
|
||||
1. Executor берёт pool pod → специализирует под функцию
|
||||
2. PHP парсер: Parse error → возвращает 500
|
||||
3. Executor считает это ошибкой вызова → ретраит следующий pod
|
||||
4. 20 ретраев × 5s = 100s → COLD TIMEOUT
|
||||
5. За это время пул (`console-php-env`) в namespace исчерпан
|
||||
|
||||
**Следствие:** php/large идёт ПОСЛЕ php/medium. К моменту его запуска пул уже разрушен → php/large тоже COLD TIMEOUT даже если бы его код был правильным.
|
||||
|
||||
### Диагностика diag_bugs.sh — что мы делали
|
||||
|
||||
Создан скрипт `bench-func/diag_bugs.sh` с 6 тестами:
|
||||
1. `php-arrow-min` — минимальный PHP с `fn($x)=>$x*2`
|
||||
2. `php-version` — только `return PHP_VERSION` (стандартный PHP)
|
||||
3. `php-med-noarr` — medium логика через foreach вместо arrow
|
||||
4. `ruby-med-1/2/3` — ruby/medium ×3 независимых функции
|
||||
|
||||
### Ошибки в процессе диагностики
|
||||
|
||||
**Ошибка 1: SIGPIPE при `| tee`**
|
||||
Первый прогон завершился досрочно — скрипт через SSH pipe убивался SIGPIPE при прокрутке/чтении терминала. Исправлено: nohup + прямая запись в файл.
|
||||
|
||||
**Ошибка 2: 5 параллельных прогонов diag_bugs.sh**
|
||||
В процессе диагностики случайно накопилось 5+ одновременных процессов `diag_bugs.sh` на VM (PIDs: 2476997, 2478955, 2480445, 2482045, 2482452). Все конкурировали за PHP pool → дополнительный pool exhaustion.
|
||||
|
||||
**Следствие:** тесты `php-version` и `php-med-noarr` тоже дали COLD TIMEOUT — не из-за собственного бага, а из-за занятого пула.
|
||||
|
||||
### Что реально подтверждено vs что предполагается
|
||||
|
||||
| Факт | Статус |
|
||||
|------|--------|
|
||||
| `fn()=>` вызывает COLD TIMEOUT | **ПОДТВЕРЖДЕНО** (×2 прогона) |
|
||||
| PHP env версия < 7.4 | **ПРЕДПОЛОЖЕНИЕ** (php-version не дал ответа из-за pool exhaustion) |
|
||||
| php-version FAIL = самостоятельный баг | **ОПРОВЕРГНУТО** — следствие pool exhaustion |
|
||||
| php-med-noarr FAIL = PHP env broken | **НЕ ПОДТВЕРЖДЕНО** — тест не прошёл чисто |
|
||||
|
||||
### Что НЕ удалось выяснить
|
||||
- Точная версия PHP в env образе
|
||||
- Работает ли `php-med-noarr` (medium без arrow) в изолированном тесте
|
||||
|
||||
---
|
||||
|
||||
## Механизм pool exhaustion — общая проблема
|
||||
|
||||
Одна функция с синтаксической ошибкой может "заморозить" весь PHP пул namespace:
|
||||
1. Сломанная функция → 20 ретраев × пул pod
|
||||
2. Пул исчерпан → **все остальные PHP функции того же пользователя** получают COLD TIMEOUT
|
||||
3. Пользователь видит что "рабочая" функция сломалась — не понимает почему
|
||||
|
||||
В реальном проде это критично: один неосторожный деплой = все PHP функции пользователя не отвечают.
|
||||
|
||||
### Почему pod "держится"
|
||||
Pool pod ≠ выполнение кода. Pod — контейнер-заготовка. При invoke:
|
||||
- Executor специализирует pod (загружает код через /specialize)
|
||||
- PHP парсер падает → 500 → executor ретраит СЛЕДУЮЩИЙ pod
|
||||
- Каждый ретрай = новый pod из пула
|
||||
|
||||
Pod не "держится" в одном — executor расходует пул по одному pod'у за ретрай.
|
||||
|
||||
### Системные решения (не реализованы)
|
||||
1. **Circuit breaker в executor** — после N провалов подряд для одной функции → вернуть ошибку, не трогать пул
|
||||
2. **Синтаксическая валидация при upload** — `php -l`, `python3 -m py_compile`, `ruby -c`, `node --check` — сломанный код не попадает в систему
|
||||
3. **Pool size limit per namespace** — ограничить max pods на env × namespace
|
||||
|
||||
Вариант 2 (валидация при upload) — реализуем в нашем коде (fission-console), остальные — в fission-bundle.
|
||||
|
||||
---
|
||||
|
||||
## Предложения — что делать
|
||||
|
||||
### Приоритет 1 (быстро): исправить bench_full.sh
|
||||
Заменить arrow functions в PHP medium/large на традиционный синтаксис.
|
||||
- `fn($s) => strrev(strtoupper($s))` → `function($s){ return strrev(strtoupper($s)); }`
|
||||
- `fn($x) => ($x - $m) ** 2` → foreach цикл
|
||||
- Аналогично для large (вложенные array_map)
|
||||
|
||||
После фикса запустить bench_full.sh заново — должно быть PASS=20.
|
||||
|
||||
### Приоритет 2 (чистая диагностика): проверить php-med-noarr
|
||||
Убить фоновые процессы на VM, запустить одиночный тест php-version/php-med-noarr в изоляции — убедиться что PHP env работает для стандартного кода.
|
||||
|
||||
### Приоритет 3 (системный): валидация кода при upload
|
||||
Добавить в fission-console синтаксическую проверку кода перед созданием функции. Защита от pool exhaustion для всех пользователей.
|
||||
|
||||
---
|
||||
|
||||
## Файлы
|
||||
|
||||
- `bench-func/bench_full.sh` — основной бенчмарк (требует фикс PHP medium/large)
|
||||
- `bench-func/diag_bugs.sh` — диагностический скрипт (создан в этой сессии)
|
||||
- `test-results/bench-func/` — логи прогонов
|
||||
@@ -0,0 +1,275 @@
|
||||
# Анализ репозитория fission-console
|
||||
|
||||
Составлен: 2026-05-21. Основан только на чтении исходного кода + истории git.
|
||||
|
||||
---
|
||||
|
||||
## Что это
|
||||
|
||||
**Web UI + API proxy** для управления Fission (Kubernetes-native serverless).
|
||||
Написан на Go. Работает внутри K8s-кластера. Пользователи аутентифицируются
|
||||
через Deck API (облачный SSO).
|
||||
|
||||
## Архитектура — поток данных
|
||||
|
||||
```
|
||||
Browser (SPA) → Console API → K8s API (CRD — dynamic client)
|
||||
→ Fission Router (invoke proxy — /fission-function)
|
||||
→ storagesvc (архивы → S3 ngcloud msk-1, бакет sless-functions)
|
||||
→ Deck API (auth — prod/dev/test)
|
||||
→ PostgreSQL (billing invocations)
|
||||
→ Grafana (дашборды — по одному Org на namespace)
|
||||
```
|
||||
|
||||
### Пакеты
|
||||
|
||||
| Пакет | Роль |
|
||||
|---|---|
|
||||
| `cmd/server/` | Точка входа: парсинг env vars, создание зависимостей, запуск HTTP |
|
||||
| `internal/api/` | **Ядро**: все HTTP-хендлеры (29 файлов) |
|
||||
| `internal/auth/` | Аутентификация: Deck API + Demo (test fallback) |
|
||||
| `internal/cloud/` | Namespace lifecycle: квата, NetworkPolicy, одноразовый создаватель |
|
||||
| `internal/fission/` | Константы GVR + SetupFissionNamespace + EnsureEnvironment |
|
||||
| `internal/model/` | Общие типы: CreateFunctionRequest, LangEnvMap |
|
||||
| `internal/runtime/` | Упаковка кода: zip, ESM-обёртка NodeJS, python/phh/ruby/go |
|
||||
| `internal/billing/` | Статистика: PostgreSQL (NoopStore если BILLING_DSN пуст) |
|
||||
| `internal/stats/` | Grafana: Organization/dashboard per namespace |
|
||||
| `ui/` | Embed SPA (index.html), отдельный /cron/ UI |
|
||||
|
||||
---
|
||||
|
||||
## Фичи — детальный разбор
|
||||
|
||||
### 1. Аутентификация
|
||||
|
||||
**Файлы:** `auth.go`, `middleware.go`, `handlers.go` (строка handleAuth)
|
||||
|
||||
- `POST /console/api/auth` — тело `{token, env}`. Валидация через Deck API.
|
||||
Возвращает `{ok, env, namespace, email}`.
|
||||
- **Middleware** — каждый приватный роут → `Authenticator.Authenticate(token, env)`.
|
||||
Без токена → 401.
|
||||
- **Namespace** = `"fission-"` + hex(SHA256(sub)[:8]). Для test-пользователей:
|
||||
`"fission-test-"` + hex(SHA256(sub)[:8]).
|
||||
- **DeckAuthenticator** — HTTP-запрос к `deck-api-{env}.ngcloud.ru/api/v1/user`.
|
||||
Кеш в `sync.Map` (ключ = `env:token`, значение = expiry time). Таймаут 5s.
|
||||
- **DemoAuthenticator** — для тестовых стендов: sub = email = token, без проверки.
|
||||
- **MultiAuthenticator** — последовательно пробует JWT → Demo.
|
||||
|
||||
### 2. Управление Namespace
|
||||
|
||||
**Файлы:** `cloud/tenant.go`, `cloud/quota.go`, `cloud/network.go`
|
||||
|
||||
- **EnsureUserNS** — единая точка входа. Singleflight (параллельные запросы
|
||||
для одного NS ждут на chan). Семафор: не более 3 одновременных созданий.
|
||||
In-memory кэш уже созданных NS.
|
||||
- **Что создаётся при первом запросе пользователя:**
|
||||
- Namespace с лейблами `managed-by=fission-console`, `fission.io/managed=true`
|
||||
- ServiceAccounts: `fission-fetcher`, `fission-builder`
|
||||
- RoleBindings: для executor, router, buildermgr, kubewatcher, timer, fetcher, builder
|
||||
- ResourceQuota (из env vars, по умолчанию: CPU req 1/lim 12, mem 2Gi/6Gi, pods 30, functions 20)
|
||||
- LimitRange (дефолтный CPU 500m, mem 256Mi на контейнер)
|
||||
- NetworkPolicy `deny-cross-tenant` — ingress только из своего NS, fission NS, kube-system
|
||||
- **ExpiryReaper** — горутина в main, раз в 5 минут (REAPER_INTERVAL). Удаляет
|
||||
функции с аннотацией `expires_at` в прошлом.
|
||||
- **NSReconciler** — сверяет FISSION_RESOURCE_NAMESPACES.
|
||||
|
||||
### 3. CRUD функций
|
||||
|
||||
**Файлы:** `function_code.go`, `function_archive.go`, `function_crud.go`,
|
||||
`function_clone.go`, `storagesvc.go`
|
||||
|
||||
#### Создание из кода (POST /console/api/functions, JSON)
|
||||
|
||||
- Тело: `{name, language, code, deps?, entrypoint?, route?, methods?, timeout?, ttl?}`
|
||||
- **Валидация:** имя `^[a-z0-9]([a-z0-9-]*[a-z0-9])?$`, ≤57 символов, код ≤1MB.
|
||||
- **Языки:** python, nodejs, php, ruby, go.
|
||||
- **Порядок:** Package CRD → Function CRD → HTTPTrigger CRD. При ошибке откат.
|
||||
- **Типы пакетов:**
|
||||
- Python: zip с кодом + опционально requirements.txt (если deps не пуст)
|
||||
- NodeJS: ESM-wrapped zip (package.json + main.js с `__invoke` dispatcher)
|
||||
- PHP: zip с `main.php` + опционально composer.json
|
||||
- Ruby: zip с `handler.rb` + опционально Gemfile
|
||||
- Go: source package (builder компилирует .so плагин)
|
||||
- **Загрузка:** через storagesvc → S3 (ngcloud msk-1, бакет `sless-functions`).
|
||||
Fallback на type:literal если storagesvcURL пуст.
|
||||
- **TTL:** парсит `1h`, `30m`, `7d`, `24h`. Ставит аннотацию `expires_at` на
|
||||
Function CRD в `metadata.annotations`.
|
||||
- **Env vars:** сохраняются в аннотации `fission-console/env-vars` (JSON).
|
||||
|
||||
#### Создание из архива (POST /console/api/functions, multipart/form-data)
|
||||
|
||||
- Поле формы: `archive` (zip-файл).
|
||||
- Проверка magic bytes: `PK` (0x50 0x4B).
|
||||
- Размер ≤32MB.
|
||||
- Загружается в storagesvc как есть, без модификации.
|
||||
- Аннотация `fission-console/source-type: "archive"`.
|
||||
- Поля формы: `name, language/environment, entrypoint, route, methods, timeout, ttl`.
|
||||
|
||||
#### GET функции (GET /console/api/functions/:name)
|
||||
|
||||
- Возвращает: name, namespace, environment, package, entrypoint, timeout,
|
||||
created_at, updated_at, code, deps, source_type, archive_filename, route,
|
||||
methods, env_vars, raw CRD.
|
||||
- **Извлечение кода:** пробует source.literal → deployment.literal → URL
|
||||
(через storagesvc download для type:url). Для NodeJS распознаёт и анрапает
|
||||
ESM-обёртку (`decodeArchiveBytesToSource` в `package.go`).
|
||||
|
||||
#### Обновление кода (PUT /console/api/functions/:name/code)
|
||||
|
||||
- Тело: `{code, deps?, timeout?}`
|
||||
- Создаёт **новый Package** (новое имя = cache miss в executor). Старый удаляется.
|
||||
- Та же сборка архива что и при создании (buildDeployArchive).
|
||||
|
||||
#### Обновление архива (PUT /console/api/functions/:name/archive)
|
||||
|
||||
- multipart/form-data, поле `archive`. Загружается в storagesvc, новый Package.
|
||||
|
||||
#### Клонирование (POST /console/api/functions/:name/clone)
|
||||
|
||||
- Тело: `{new_name, route?}`
|
||||
- Для type:literal — копирует base64 literal в новый Package.
|
||||
- Для type:url — скачивает архив из storagesvc → заливает новый → новый Package.
|
||||
- Создаёт новый Function + HTTPTrigger. Для Go копирует source-пакет.
|
||||
- При ошибке откатывает созданные объекты.
|
||||
|
||||
#### DELETE (DELETE /console/api/functions/:name)
|
||||
|
||||
- Каскадное: удаляет HTTPTrigger → Package CRD → удаляет архив из storagesvc
|
||||
(DELETE /v1/archive?id=... через storagesvc).
|
||||
- Проверяет что функция реально удалена из K8s.
|
||||
|
||||
#### Timeout, Logs, EnvVars
|
||||
|
||||
- `PUT /functions/:name/timeout` — патчит только `spec.functionTimeout`
|
||||
(без замены кода/архива).
|
||||
- `GET /functions/:name/logs` — читает логи пода функции через K8s API
|
||||
(rest client, `pod/log`).
|
||||
- `GET /functions/:name/envvars` / `PUT /functions/:name/envvars` — аннотация
|
||||
`fission-console/env-vars`.
|
||||
|
||||
### 4. Invoke (вызов функций)
|
||||
|
||||
**Файл:** `function_invoke.go`
|
||||
|
||||
- **`POST /console/api/functions/:name/invoke`** — для UI. Проксирует через
|
||||
Fission Router. URL: `routerURL/fission-function/<ns>/<name>`.
|
||||
Ответ: `{status, body, headers, cold_start, response_raw, pod, duration_ms}`.
|
||||
Записывает в billing.
|
||||
- **`/fn/<route>`** — публичный gateway. Строит URL по маршруту, проксирует
|
||||
тело/заголовки (кроме служебных). Таймаут из `spec.functionTimeout`.
|
||||
- **`/fission-function/<ns>/<name>`** — внутренний gateway для cron/timer/webhook.
|
||||
- JWT-токен router: ServiceAccount token из `/var/run/secrets/...` (или SA_TOKEN_PATH),
|
||||
кешируется на 12ч в `sync.Mutex`.
|
||||
|
||||
### 5. Триггеры
|
||||
|
||||
**Файлы:** `timetriggers.go`, `mqtriggers.go`, `kwtriggers.go`
|
||||
|
||||
- **HTTP Triggers** — создаются автоматически при создании функции.
|
||||
Чтение списка: GET `/console/api/httptriggers`.
|
||||
- **Time Triggers** — CRUD через `/console/api/timetriggers`.
|
||||
Поля: `name, functionName, cron, method, subpath`. Создаются как
|
||||
`fission.io/v1 TimeTrigger` CRD.
|
||||
- **MQ Triggers** — `/console/api/mqtriggers`. Поля: `name, functionName,
|
||||
queue, sqsEndpoint, accessKey, secretKey`. Создаёт Deployment (sqs-consumer)
|
||||
+ Secret с credentials в NS пользователя. sqs-consumer поллит SQS →
|
||||
вызывает функцию через JWT-authenticated router.
|
||||
- **Kubernetes Watch Triggers** — `/console/api/kwtriggers`. Поля:
|
||||
`name, functionName, resourceType, namespace, labelSelector`.
|
||||
|
||||
### 6. AI/Lint фичи
|
||||
|
||||
**Файлы:** `ai_check.go`, `lint_archive.go`, `explain_archive.go`, `ai_prompts.go`
|
||||
|
||||
- **`POST /ai/check`** — синтаксическая проверка кода. Реальные линтеры
|
||||
(не LLM): `node --check`, `python3 -m py_compile`, `ruby -c`, `php -l`,
|
||||
Go через `go/parser` прямо в процессе.
|
||||
- **`POST /ai/lint-archive`** — принимает zip, линтит каждый файл с
|
||||
известным расширением. Защита от zip bomb: max 100KB архив, max 100KB
|
||||
распакованных. Опционально: проверка entrypoint и соответствия языка.
|
||||
- **`POST /ai/explain-archive`** — извлекает код из архива (max 20KB),
|
||||
отправляет в LLM через OpenRouter API с вопросом "что делает?".
|
||||
- **`POST /ai/ask`** — chat/генерация кода через LLM.
|
||||
4 режима codegen: генерация с нуля, объяснение, исправление ошибки,
|
||||
универсальный. Плюс explain и chat режимы.
|
||||
- **Prompts** (`ai_prompts.go`) — шаблоны для каждого языка с сигнатурами
|
||||
функций (Python: `def main(event, context)`, NodeJS: `module.exports`,
|
||||
PHP: `function handler(...)`, Ruby: `def handler(context)`,
|
||||
Go: `func Handler(...)`).
|
||||
|
||||
### 7. Billing (статистика)
|
||||
|
||||
**Файлы:** `billing/billing.go`, `billing/pg.go`, `billing/noop.go`,
|
||||
`billing/factory.go`
|
||||
|
||||
- **Интерфейс Store** — `RecordInvocation(Invocation)`. Асинхронный,
|
||||
fire-and-forget.
|
||||
- **pgStore** — PostgreSQL через pgxpool. Таблица `invocations`.
|
||||
Пулы: max 4 conn, min 1. Каждая запись в своей горутине.
|
||||
- **NoopStore** — если `BILLING_DSN` не задан или не удалось подключиться —
|
||||
статистика молча отключена.
|
||||
- **Поля Invocation:** Namespace, FunctionName, TriggerType (http/cron/console/event),
|
||||
Route, HTTPMethod, StartedAt, DurationMS, StatusCode, ColdStart,
|
||||
RequestBytes, ResponseBytes, ErrorMsg, RecordedBy ("console"/"router"),
|
||||
EventType (create/delete/clone/update/invoke).
|
||||
- **Где записывается:** handleInvokeFunction, handleInvokeRoute,
|
||||
handleFissionFunctionGateway, handleCreateFunction, handleDeleteFunction,
|
||||
handleUpdateFunctionCode, handleCloneFunction.
|
||||
|
||||
### 8. Stats / Grafana
|
||||
|
||||
**Файлы:** `stats/grafana.go`, `stats/factory.go`, `stats/noop.go`,
|
||||
`stats/provider.go`
|
||||
|
||||
- **GrafanaProvider** — для каждого namespace создаёт Grafana Organization
|
||||
(имя = namespace), PostgreSQL datasource (тот же DSN что billing,
|
||||
uid="fission-user-pg"), hardcoded dashboard (SQL: WHERE namespace='...'),
|
||||
public dashboard link (accessToken).
|
||||
- **Кеш:** `sync.RWMutex` + per-namespace кеш orgID и accessToken.
|
||||
Идемпотентно — если Org уже есть, повторно не создаётся.
|
||||
- **NoopProvider** — если `GRAFANA_INTERNAL_URL` не задан.
|
||||
- **Провизия:** вызывается в `handleAuth` (fire-and-forget горутина),
|
||||
и в `handleStatsDashboard`.
|
||||
|
||||
### 9. UI
|
||||
|
||||
**Файлы:** `ui/embed.go`, `ui/cron.go`
|
||||
|
||||
- **Embedded SPA** — `//go:embed index.html`. Раздаётся на `/console/`.
|
||||
- **Cron UI** — отдельный handler для `/cron/` (Cron метрики, статика).
|
||||
- **CORS:** `Access-Control-Allow-Origin: *` (все операции защищены токеном,
|
||||
не cookie).
|
||||
- **Security headers:** nosniff, X-Frame-Options DENY, CSP (default-src 'self'),
|
||||
Referrer-Policy strict-origin-when-cross-origin, Permissions-Policy (нет камере/микрофону).
|
||||
|
||||
### 10. Мониторинг
|
||||
|
||||
- `/health`, `/console/health` — `ok\n`
|
||||
- `/console/api/ns/status` — статус namespace (существует, кол-во функций)
|
||||
- `/console/api/ns/debug` — дебаг-инфо (NS, user, labels)
|
||||
- `/console/api/stats/dashboard-url` — возвращает Grafana public dashboard URL
|
||||
|
||||
---
|
||||
|
||||
## Эволюция storagesvc (история git)
|
||||
|
||||
- **До 19 мая 2026:** S3 (изначальная архитектура)
|
||||
- **19 мая:** `3eb4f56 feat: PV storage backend` — перешли на PVC (временное решение)
|
||||
- **20 мая:** `7539680 chore: switch storagesvc to S3 backend (ngcloud msk-1)` —
|
||||
вернулись на S3. PV-конфиг сохранён в `helm/fission-values-pv.yaml` для быстрого
|
||||
отката.
|
||||
- **Текущее состояние (21 мая):** S3, бакет `sless-functions`, endpoint
|
||||
`https://s3.msk-1.ngcloud.ru`. Ветка `feat/s3-replicated`.
|
||||
|
||||
---
|
||||
|
||||
## Чего нет
|
||||
|
||||
- **Нет аутентификации через cookie** — только токен в заголовке.
|
||||
- **Нет rate limiting** — только ResourceQuota на уровне K8s.
|
||||
- **Нет WebSocket** — только REST.
|
||||
- **Нет аудит-лога** — billing таблица не immutable, можно переписать.
|
||||
- **Нет swagger/OpenAPI** — документация только в doc/.
|
||||
- **Нет gRPC** — только HTTP/JSON.
|
||||
- **Нет метрик Prometheus** — только billing в PostgreSQL.
|
||||
- **Нет CI/CD** — тесты запускаются вручную.
|
||||
@@ -0,0 +1,30 @@
|
||||
# Fission helm values — PV (vcd-disk-ext4) backend
|
||||
# Применять: helm upgrade fission fission-charts/fission-all --version 1.22.1 \
|
||||
# -n fission -f helm/fission-values-pv.yaml
|
||||
#
|
||||
# Секретов нет — local storage не требует credentials.
|
||||
# ПЕРЕД upgrade: kubectl delete deploy executor router -n fission
|
||||
|
||||
authentication:
|
||||
enabled: true
|
||||
|
||||
defaultNamespace: fission-functions
|
||||
|
||||
image: naeel/fission-bundle
|
||||
imageTag: v1.23.0
|
||||
fetcher:
|
||||
image: naeel/fetcher
|
||||
imageTag: v1.23.0
|
||||
|
||||
persistence:
|
||||
enabled: true
|
||||
storageType: local
|
||||
storageClass: vcd-disk-ext4
|
||||
size: 2Gi
|
||||
accessMode: ReadWriteOnce
|
||||
|
||||
repository: ""
|
||||
routerServiceType: ClusterIP
|
||||
serviceType: ClusterIP
|
||||
|
||||
analytics: false
|
||||
@@ -0,0 +1,34 @@
|
||||
# Fission helm values — S3 backend (ngcloud msk-1)
|
||||
# Применять: helm upgrade fission fission-charts/fission-all --version 1.22.1 \
|
||||
# -n fission -f helm/fission-values.yaml -f helm/fission-s3-secret.yaml
|
||||
#
|
||||
# Секреты — в helm/fission-s3-secret.yaml (в .gitignore, не коммитить!)
|
||||
# Вернуться на PV: -f helm/fission-values-pv.yaml (без s3-secret)
|
||||
# ПЕРЕД upgrade: kubectl delete deploy executor router -n fission
|
||||
|
||||
authentication:
|
||||
enabled: true
|
||||
|
||||
defaultNamespace: fission-functions
|
||||
|
||||
image: naeel/fission-bundle
|
||||
imageTag: v1.23.0
|
||||
fetcher:
|
||||
image: naeel/fetcher
|
||||
imageTag: v1.23.0
|
||||
|
||||
persistence:
|
||||
enabled: true
|
||||
storageType: s3
|
||||
s3:
|
||||
bucketName: sless-functions
|
||||
subDir: fission
|
||||
region: msk-1
|
||||
endPoint: https://s3.msk-1.ngcloud.ru
|
||||
# accessKeyId и secretAccessKey — в fission-s3-secret.yaml
|
||||
|
||||
repository: ""
|
||||
routerServiceType: ClusterIP
|
||||
serviceType: ClusterIP
|
||||
|
||||
analytics: false
|
||||
@@ -1,5 +1,8 @@
|
||||
# Демо-пайплайн: IoT → SQS → Fission
|
||||
|
||||
> ⚠️ LEGACY — написано до реализации MQ-триггера.
|
||||
> Сейчас MQ-триггер реализован (см. TODO.txt).
|
||||
|
||||
## Архитектура
|
||||
|
||||
1. **Крон-функция (Fission)**
|
||||
@@ -20,6 +23,9 @@
|
||||
|
||||
## Требования к Fission
|
||||
|
||||
> ⚠️ LEGACY: MQ-триггер реализован через sqs-consumer (май 2026).
|
||||
> Этот раздел оставлен для исторического контекста.
|
||||
|
||||
- Необходим MQ-триггер (MessageQueue Trigger) для автоматического запуска функции по сообщениям из очереди.
|
||||
- Сейчас в Fission есть HTTP, Cron, Event, но нет универсального MQ-триггера.
|
||||
- Возможные варианты:
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
migrate_s3_to_pv.py — перелив архивов функций из S3 → storagesvc (PV-backend)
|
||||
|
||||
Запуск: kubectl exec -n fission deploy/storagesvc -- python3 /tmp/migrate.py
|
||||
ИЛИ: запустить как Job в кластере
|
||||
|
||||
Что делает:
|
||||
1. Читает все Package CRD во всех namespace
|
||||
2. Для каждого пакета с URL s3://... скачивает архив из S3
|
||||
3. Загружает в storagesvc HTTP API → получает новый URL
|
||||
4. Обновляет Package CRD: .spec.deployment.url = новый URL
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import time
|
||||
import urllib.request
|
||||
import urllib.parse
|
||||
|
||||
# S3 credentials (из env или хардкод для ручного запуска)
|
||||
S3_ENDPOINT = os.environ.get("S3_ENDPOINT", "https://s3.msk-1.ngcloud.ru")
|
||||
S3_BUCKET = os.environ.get("S3_BUCKET", "sless-functions")
|
||||
S3_ACCESS_KEY = os.environ.get("S3_ACCESS_KEY", "0GLQRD38H4I6RBDB0EWJ")
|
||||
S3_SECRET_KEY = os.environ.get("S3_SECRET_KEY", "eTFibiHmBd96IApj9PYsboTR6OBoD7osxoarHykw")
|
||||
S3_REGION = os.environ.get("S3_REGION", "msk-1")
|
||||
|
||||
STORAGESVC_URL = os.environ.get("STORAGESVC_URL", "http://storagesvc.fission.svc.cluster.local")
|
||||
|
||||
import subprocess
|
||||
|
||||
def kubectl(args, input_data=None):
|
||||
cmd = ["kubectl"] + args
|
||||
r = subprocess.run(cmd, capture_output=True, text=True, input=input_data)
|
||||
if r.returncode != 0:
|
||||
raise RuntimeError(f"kubectl {' '.join(args)} failed: {r.stderr}")
|
||||
return r.stdout
|
||||
|
||||
def get_all_packages():
|
||||
out = kubectl(["get", "packages", "--all-namespaces", "-o", "json"])
|
||||
return json.loads(out)["items"]
|
||||
|
||||
def is_s3_url(url):
|
||||
return url and url.startswith("s3://")
|
||||
|
||||
def s3_key_from_url(url):
|
||||
# s3://sless-functions/fission/abc123 → fission/abc123
|
||||
path = url[len(f"s3://{S3_BUCKET}/"):]
|
||||
return path
|
||||
|
||||
def download_from_s3(s3_key):
|
||||
import boto3
|
||||
s3 = boto3.client(
|
||||
"s3",
|
||||
endpoint_url=S3_ENDPOINT,
|
||||
aws_access_key_id=S3_ACCESS_KEY,
|
||||
aws_secret_access_key=S3_SECRET_KEY,
|
||||
region_name=S3_REGION,
|
||||
)
|
||||
obj = s3.get_object(Bucket=S3_BUCKET, Key=s3_key)
|
||||
return obj["Body"].read()
|
||||
|
||||
def upload_to_storagesvc(data):
|
||||
import urllib.request, uuid
|
||||
boundary = uuid.uuid4().hex
|
||||
body = (
|
||||
f"--{boundary}\r\n"
|
||||
f'Content-Disposition: form-data; name="uploadfile"; filename="archive.zip"\r\n'
|
||||
f"Content-Type: application/octet-stream\r\n\r\n"
|
||||
).encode() + data + f"\r\n--{boundary}--\r\n".encode()
|
||||
|
||||
req = urllib.request.Request(
|
||||
f"{STORAGESVC_URL}/v1/archive",
|
||||
data=body,
|
||||
method="POST",
|
||||
headers={"Content-Type": f"multipart/form-data; boundary={boundary}"},
|
||||
)
|
||||
with urllib.request.urlopen(req, timeout=60) as resp:
|
||||
result = json.loads(resp.read())
|
||||
return result["id"] # storagesvc возвращает {"id": "http://storagesvc.../v1/archive?id=..."}
|
||||
|
||||
def update_package_url(ns, name, new_url):
|
||||
patch = json.dumps({"spec": {"deployment": {"url": new_url, "type": "url"}}})
|
||||
kubectl(["patch", "package", name, "-n", ns, "--type=merge", f"--patch={patch}"])
|
||||
|
||||
def main():
|
||||
try:
|
||||
import boto3
|
||||
except ImportError:
|
||||
print("ERROR: boto3 не установлен. Запусти: pip install boto3")
|
||||
sys.exit(1)
|
||||
|
||||
packages = get_all_packages()
|
||||
total = len(packages)
|
||||
migrated = 0
|
||||
skipped = 0
|
||||
errors = 0
|
||||
|
||||
print(f"Всего пакетов: {total}")
|
||||
print()
|
||||
|
||||
for pkg in packages:
|
||||
ns = pkg["metadata"]["namespace"]
|
||||
name = pkg["metadata"]["name"]
|
||||
url = pkg.get("spec", {}).get("deployment", {}).get("url", "")
|
||||
|
||||
if not is_s3_url(url):
|
||||
print(f" SKIP {ns}/{name} — url: {url or '(empty)'}")
|
||||
skipped += 1
|
||||
continue
|
||||
|
||||
s3_key = s3_key_from_url(url)
|
||||
print(f" MIG {ns}/{name} s3://{S3_BUCKET}/{s3_key}", end=" ", flush=True)
|
||||
|
||||
try:
|
||||
data = download_from_s3(s3_key)
|
||||
new_id = upload_to_storagesvc(data)
|
||||
new_url = f"{STORAGESVC_URL}/v1/archive?id={new_id}" if not new_id.startswith("http") else new_id
|
||||
update_package_url(ns, name, new_url)
|
||||
print(f"→ {new_url}")
|
||||
migrated += 1
|
||||
except Exception as e:
|
||||
print(f" ERROR: {e}")
|
||||
errors += 1
|
||||
|
||||
print()
|
||||
print(f"Готово: перенесено={migrated} пропущено={skipped} ошибок={errors}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Executable
+994
@@ -0,0 +1,994 @@
|
||||
#!/usr/bin/env bash
|
||||
# test_heavy.sh — поиск багов: большой код, CPU-нагрузка, большой ответ,
|
||||
# архивы из ZIP, быстрые серии invoke, параллельные вызовы,
|
||||
# медленные функции, давление по памяти.
|
||||
# Все 4 языка: Python, Node.js, Ruby, PHP.
|
||||
#
|
||||
# ВАЖНО: Ruby и PHP invoke ранее не тестировались вообще!
|
||||
# Умеренная нагрузка (кластер небольшой, но нагрузка реальная).
|
||||
#
|
||||
# Запуск: bash scripts/test_heavy.sh
|
||||
# На ВМ: bash ~/terra/fission/scripts/test_heavy.sh
|
||||
set -uo pipefail
|
||||
|
||||
ROOT_DIR=$(cd "$(dirname "$0")" && pwd)
|
||||
source "${ROOT_DIR}/lib.sh"
|
||||
|
||||
RUN_ID=$(mk_run_id)
|
||||
SUB="livetest@test.local"
|
||||
TMP=$(mktemp -d)
|
||||
|
||||
echo ""
|
||||
echo "════════════════════════════════════════════════════════"
|
||||
echo " test_heavy.sh run=${RUN_ID}"
|
||||
echo "════════════════════════════════════════════════════════"
|
||||
|
||||
declare -a ALL_FNS=()
|
||||
|
||||
cleanup() {
|
||||
rm -rf "$TMP" 2>/dev/null || true
|
||||
for fn in "${ALL_FNS[@]:-}"; do
|
||||
curl -s -X DELETE "${BASE}/functions/${fn}" \
|
||||
-H "X-Auth-Token: ${SUB}" >/dev/null 2>&1 || true
|
||||
done
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
# ─── helpers ───────────────────────────────────────────────────────────────
|
||||
|
||||
# reg FN — регистрирует имя функции для cleanup
|
||||
reg() { ALL_FNS+=("$1"); }
|
||||
|
||||
# create_json LANG FN CODE_FILE — создаёт функцию из файла с кодом
|
||||
create_json() {
|
||||
local lang="$1" fn="$2" codefile="$3"
|
||||
local code_escaped
|
||||
code_escaped=$(cat "$codefile" | json_escape)
|
||||
reg "$fn"
|
||||
local R
|
||||
R=$(curl -s -w "\n%{http_code}" -X POST "${BASE}/functions" \
|
||||
-H "X-Auth-Token: ${SUB}" -H "Content-Type: application/json" \
|
||||
-d "{\"name\":\"${fn}\",\"language\":\"${lang}\",\"code\":${code_escaped}}" 2>/dev/null)
|
||||
check_http "$(echo "$R" | tail -1)" "201" "${lang^^} ${fn} CREATE → 201"
|
||||
}
|
||||
|
||||
# invoke_once FN EXPECTED LABEL — один invoke, без retry
|
||||
invoke_once() {
|
||||
local fn="$1" expected="$2" label="$3"
|
||||
local resp status raw err
|
||||
resp=$(curl -s --max-time 60 -X POST "${BASE}/functions/${fn}/invoke" \
|
||||
-H "X-Auth-Token: ${SUB}" -H "Content-Type: application/json" -d '{}')
|
||||
status=$(json_field "$resp" status)
|
||||
raw=$(json_field "$resp" response_raw)
|
||||
err=$(json_field "$resp" error)
|
||||
echo " fn=$fn status=$status raw=$(printf '%s' "$raw" | head -c 100) err=$(printf '%s' "$err" | head -c 80)"
|
||||
if [ "$status" = "200" ] && printf '%s' "$raw" | grep -qF "$expected"; then
|
||||
pass "$label → 200 + '${expected}'"
|
||||
return 0
|
||||
else
|
||||
fail "$label → status=${status:-?} (ожидали 200 + '${expected}')"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
# SECTION 1: БОЛЬШОЙ КОД — ~100 строк на язык, из inline-кода
|
||||
# Ищем: лимиты на размер кода, проблемы загрузки, синтаксические баги
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
echo ""
|
||||
echo "────────────────────────────────────────────────────────"
|
||||
echo " S1: БОЛЬШОЙ КОД (Python / Node.js / Ruby / PHP)"
|
||||
echo "────────────────────────────────────────────────────────"
|
||||
|
||||
FN_BIG_PY="t-big-py-${RUN_ID}"
|
||||
cat > "${TMP}/big_py.py" << 'PYEOF'
|
||||
import math, hashlib, json
|
||||
|
||||
CONSTS = {f"k{i}": i * 3.14159265 for i in range(20)}
|
||||
|
||||
def fib(n):
|
||||
a, b = 0, 1
|
||||
for _ in range(n - 1): a, b = b, a + b
|
||||
return b
|
||||
|
||||
def sieve(limit):
|
||||
is_p = bytearray([1]) * (limit + 1)
|
||||
is_p[0] = is_p[1] = 0
|
||||
for i in range(2, int(limit**0.5) + 1):
|
||||
if is_p[i]:
|
||||
for j in range(i*i, limit+1, i): is_p[j] = 0
|
||||
return sum(is_p)
|
||||
|
||||
def rot13(s):
|
||||
return s.translate(str.maketrans(
|
||||
'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ',
|
||||
'nopqrstuvwxyzabcdefghijklmNOPQRSTUVWXYZABCDEFGHIJKLM'))
|
||||
|
||||
def caesar(s, shift=7):
|
||||
res = []
|
||||
for c in s:
|
||||
if 'a' <= c <= 'z': res.append(chr((ord(c) - 97 + shift) % 26 + 97))
|
||||
elif 'A' <= c <= 'Z': res.append(chr((ord(c) - 65 + shift) % 26 + 65))
|
||||
else: res.append(c)
|
||||
return ''.join(res)
|
||||
|
||||
def mat_trace(m):
|
||||
return sum(m[i][i] for i in range(len(m)))
|
||||
|
||||
def palindrome(s):
|
||||
t = s.lower().replace(' ', '')
|
||||
return t == t[::-1]
|
||||
|
||||
def flatten(lst):
|
||||
out = []
|
||||
for item in lst:
|
||||
if isinstance(item, list): out.extend(flatten(item))
|
||||
else: out.append(item)
|
||||
return out
|
||||
|
||||
def checksum(data):
|
||||
return hashlib.sha256(data.encode()).hexdigest()[:16]
|
||||
|
||||
def running_sum(lst):
|
||||
total = 0; out = []
|
||||
for v in lst: total += v; out.append(total)
|
||||
return out
|
||||
|
||||
def count_distinct(lst):
|
||||
return len(set(lst))
|
||||
|
||||
def group_by_mod(lst, n):
|
||||
g = {i: [] for i in range(n)}
|
||||
for v in lst: g[v % n].append(v)
|
||||
return {str(k): len(v) for k, v in g.items()}
|
||||
|
||||
def pascal_row(n):
|
||||
row = [1]
|
||||
for i in range(1, n + 1): row.append(row[-1] * (n - i + 1) // i)
|
||||
return row
|
||||
|
||||
def deduplicate(lst):
|
||||
seen = set(); out = []
|
||||
for v in lst:
|
||||
if v not in seen: seen.add(v); out.append(v)
|
||||
return out
|
||||
|
||||
def reverse_words(s):
|
||||
return ' '.join(s.split()[::-1])
|
||||
|
||||
def count_vowels(s):
|
||||
return sum(1 for c in s.lower() if c in 'aeiou')
|
||||
|
||||
def is_prime(n):
|
||||
if n < 2: return False
|
||||
if n == 2: return True
|
||||
if n % 2 == 0: return False
|
||||
for i in range(3, int(math.sqrt(n)) + 1, 2):
|
||||
if n % i == 0: return False
|
||||
return True
|
||||
|
||||
def main(event, context):
|
||||
mat = [[1,2,3,4],[5,6,7,8],[9,10,11,12],[13,14,15,16]]
|
||||
data = list(range(1, 51))
|
||||
nested = [[1,[2,3]],[4,[5,[6,7]]],8]
|
||||
result = {
|
||||
"tag": "big-code-py-ok",
|
||||
"fib_25": fib(25),
|
||||
"primes_50": sieve(50),
|
||||
"rot13": rot13("Hello Fission"),
|
||||
"caesar": caesar("heavy"),
|
||||
"const_sum": round(sum(CONSTS.values()), 2),
|
||||
"mat_trace": mat_trace(mat),
|
||||
"palindrome": palindrome("racecar"),
|
||||
"distinct": count_distinct([1,2,2,3,3,3,4]),
|
||||
"flat_len": len(flatten(nested)),
|
||||
"checksum": checksum("fission-heavy-test"),
|
||||
"rsum_last": running_sum(data)[-1],
|
||||
"pascal_4": pascal_row(4),
|
||||
"dedup_len": len(deduplicate([1,2,2,3,3,3,4,5,5])),
|
||||
"rev_words": reverse_words("big code python test"),
|
||||
"vowels": count_vowels("fission heavy test"),
|
||||
"is_prime_97": is_prime(97),
|
||||
"groups_3": group_by_mod(data[:30], 3),
|
||||
}
|
||||
return json.dumps(result), 200
|
||||
PYEOF
|
||||
create_json "python" "$FN_BIG_PY" "${TMP}/big_py.py"
|
||||
|
||||
FN_BIG_NODE="t-big-node-${RUN_ID}"
|
||||
cat > "${TMP}/big_node.js" << 'JSEOF'
|
||||
const crypto = require('crypto');
|
||||
const CONSTS = {};
|
||||
for (let i = 0; i < 20; i++) CONSTS[`k${i}`] = i * Math.PI;
|
||||
|
||||
function fib(n) {
|
||||
let a = 0, b = 1;
|
||||
for (let i = 1; i < n; i++) { let t = a + b; a = b; b = t; }
|
||||
return b;
|
||||
}
|
||||
function sieve(limit) {
|
||||
const isP = new Uint8Array(limit + 1).fill(1);
|
||||
isP[0] = isP[1] = 0;
|
||||
for (let i = 2; i * i <= limit; i++)
|
||||
if (isP[i]) for (let j = i*i; j <= limit; j += i) isP[j] = 0;
|
||||
return isP.reduce((s, v) => s + v, 0);
|
||||
}
|
||||
function rot13(s) {
|
||||
return s.replace(/[a-zA-Z]/g, c => {
|
||||
const b = c <= 'Z' ? 65 : 97;
|
||||
return String.fromCharCode((c.charCodeAt(0) - b + 13) % 26 + b);
|
||||
});
|
||||
}
|
||||
function caesar(s, shift = 7) {
|
||||
return s.replace(/[a-zA-Z]/g, c => {
|
||||
const b = c <= 'Z' ? 65 : 97;
|
||||
return String.fromCharCode((c.charCodeAt(0) - b + shift) % 26 + b);
|
||||
});
|
||||
}
|
||||
function matTrace(m) { return m.reduce((s, row, i) => s + row[i], 0); }
|
||||
function palindrome(s) { const t = s.toLowerCase().replace(/ /g, ''); return t === t.split('').reverse().join(''); }
|
||||
function flatten(arr) { return arr.reduce((a, v) => a.concat(Array.isArray(v) ? flatten(v) : v), []); }
|
||||
function checksum(d) { return crypto.createHash('sha256').update(d).digest('hex').slice(0, 16); }
|
||||
function runningSum(arr) { let t = 0; return arr.map(v => (t += v, t)); }
|
||||
function countDistinct(arr) { return new Set(arr).size; }
|
||||
function deduplicate(arr) { const s = new Set(); return arr.filter(v => !s.has(v) && s.add(v)); }
|
||||
function groupByMod(arr, n) { const g = {}; for (let i = 0; i < n; i++) g[i] = 0; arr.forEach(v => g[v % n]++); return g; }
|
||||
function pascalRow(n) { const r = [1]; for (let i = 1; i <= n; i++) r.push((r[i-1] * (n-i+1) / i) | 0); return r; }
|
||||
function reverseWords(s) { return s.split(' ').reverse().join(' '); }
|
||||
function countVowels(s) { return (s.toLowerCase().match(/[aeiou]/g) || []).length; }
|
||||
function isPrime(n) {
|
||||
if (n < 2) return false;
|
||||
for (let i = 2; i * i <= n; i++) if (n % i === 0) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
const mat = [[1,2,3,4],[5,6,7,8],[9,10,11,12],[13,14,15,16]];
|
||||
const nested = [[1,[2,3]],[4,[5,[6,7]]],8];
|
||||
|
||||
module.exports = async function(ctx) {
|
||||
const data = Array.from({length: 50}, (_, i) => i + 1);
|
||||
const result = {
|
||||
tag: "big-code-node-ok",
|
||||
fib_25: fib(25),
|
||||
primes_50: sieve(50),
|
||||
rot13: rot13("Hello Fission"),
|
||||
caesar: caesar("heavy"),
|
||||
const_sum: Math.round(Object.values(CONSTS).reduce((a,b) => a+b, 0) * 100) / 100,
|
||||
mat_trace: matTrace(mat),
|
||||
palindrome: palindrome("racecar"),
|
||||
distinct: countDistinct([1,2,2,3,3,3,4]),
|
||||
flat_len: flatten(nested).length,
|
||||
checksum: checksum("fission-heavy-test"),
|
||||
rsum_last: runningSum(data).at(-1),
|
||||
pascal_4: pascalRow(4),
|
||||
dedup_len: deduplicate([1,2,2,3,3,3,4,5,5]).length,
|
||||
rev_words: reverseWords("big code node test"),
|
||||
vowels: countVowels("fission heavy test"),
|
||||
is_prime_97: isPrime(97),
|
||||
groups_3: groupByMod(data.slice(0,30), 3),
|
||||
};
|
||||
return { status: 200, body: JSON.stringify(result) };
|
||||
};
|
||||
JSEOF
|
||||
create_json "nodejs" "$FN_BIG_NODE" "${TMP}/big_node.js"
|
||||
|
||||
FN_BIG_RUBY="t-big-ruby-${RUN_ID}"
|
||||
cat > "${TMP}/big_ruby.rb" << 'RBEOF'
|
||||
require 'json'
|
||||
require 'digest'
|
||||
|
||||
CONSTS = (0...20).map { |i| [:"k#{i}", i * Math::PI] }.to_h
|
||||
|
||||
def fib(n)
|
||||
a, b = 0, 1
|
||||
(n - 1).times { a, b = b, a + b }
|
||||
b
|
||||
end
|
||||
|
||||
def sieve(limit)
|
||||
is_p = Array.new(limit + 1, true)
|
||||
is_p[0] = is_p[1] = false
|
||||
(2..Math.sqrt(limit).to_i).each do |i|
|
||||
next unless is_p[i]
|
||||
(i*i..limit).step(i) { |j| is_p[j] = false }
|
||||
end
|
||||
is_p.count(true)
|
||||
end
|
||||
|
||||
def rot13(s)
|
||||
s.tr('a-zA-Z', 'n-za-mN-ZA-M')
|
||||
end
|
||||
|
||||
def caesar(s, shift = 7)
|
||||
s.chars.map do |c|
|
||||
if c =~ /[a-z]/ then ((c.ord - 97 + shift) % 26 + 97).chr
|
||||
elsif c =~ /[A-Z]/ then ((c.ord - 65 + shift) % 26 + 65).chr
|
||||
else c
|
||||
end
|
||||
end.join
|
||||
end
|
||||
|
||||
def mat_trace(m)
|
||||
m.each_with_index.sum { |row, i| row[i] }
|
||||
end
|
||||
|
||||
def palindrome?(s)
|
||||
t = s.downcase.delete(' ')
|
||||
t == t.reverse
|
||||
end
|
||||
|
||||
def flatten_nested(lst)
|
||||
lst.each_with_object([]) do |item, out|
|
||||
item.is_a?(Array) ? out.concat(flatten_nested(item)) : out << item
|
||||
end
|
||||
end
|
||||
|
||||
def checksum(data)
|
||||
Digest::SHA256.hexdigest(data)[0, 16]
|
||||
end
|
||||
|
||||
def running_sum(arr)
|
||||
total = 0
|
||||
arr.map { |v| total += v; total }
|
||||
end
|
||||
|
||||
def count_distinct(arr)
|
||||
arr.uniq.length
|
||||
end
|
||||
|
||||
def group_by_mod(arr, n)
|
||||
(0...n).each_with_object({}) { |i, h| h[i.to_s] = arr.count { |v| v % n == i } }
|
||||
end
|
||||
|
||||
def pascal_row(n)
|
||||
row = [1]
|
||||
(1..n).each { |i| row << row[-1] * (n - i + 1) / i }
|
||||
row
|
||||
end
|
||||
|
||||
def reverse_words(s)
|
||||
s.split.reverse.join(' ')
|
||||
end
|
||||
|
||||
def count_vowels(s)
|
||||
s.downcase.count('aeiou')
|
||||
end
|
||||
|
||||
def is_prime(n)
|
||||
return false if n < 2
|
||||
(2..Math.sqrt(n).to_i).none? { |i| n % i == 0 }
|
||||
end
|
||||
|
||||
def handler(ctx)
|
||||
mat = [[1,2,3,4],[5,6,7,8],[9,10,11,12],[13,14,15,16]]
|
||||
data = (1..50).to_a
|
||||
nested = [[1,[2,3]],[4,[5,[6,7]]],8]
|
||||
result = {
|
||||
tag: "big-code-ruby-ok",
|
||||
fib_25: fib(25),
|
||||
primes_50: sieve(50),
|
||||
rot13: rot13("Hello Fission"),
|
||||
caesar: caesar("heavy"),
|
||||
const_sum: CONSTS.values.sum.round(2),
|
||||
mat_trace: mat_trace(mat),
|
||||
palindrome: palindrome?("racecar"),
|
||||
distinct: count_distinct([1,2,2,3,3,3,4]),
|
||||
flat_len: flatten_nested(nested).length,
|
||||
checksum: checksum("fission-heavy-test"),
|
||||
rsum_last: running_sum(data).last,
|
||||
pascal_4: pascal_row(4),
|
||||
rev_words: reverse_words("big code ruby test"),
|
||||
vowels: count_vowels("fission heavy test"),
|
||||
is_prime_97: is_prime(97),
|
||||
groups_3: group_by_mod(data.first(30), 3),
|
||||
}
|
||||
{ statusCode: 200, body: result.to_json }
|
||||
end
|
||||
RBEOF
|
||||
create_json "ruby" "$FN_BIG_RUBY" "${TMP}/big_ruby.rb"
|
||||
|
||||
FN_BIG_PHP="t-big-php-${RUN_ID}"
|
||||
cat > "${TMP}/big_php.php" << 'PHPEOF'
|
||||
<?php
|
||||
$CONSTS = [];
|
||||
for ($i = 0; $i < 20; $i++) $CONSTS["k{$i}"] = $i * M_PI;
|
||||
|
||||
function fib($n) {
|
||||
$a = 0; $b = 1;
|
||||
for ($i = 1; $i < $n; $i++) { $t = $a + $b; $a = $b; $b = $t; }
|
||||
return $b;
|
||||
}
|
||||
function sieve($limit) {
|
||||
$isP = array_fill(0, $limit + 1, true);
|
||||
$isP[0] = $isP[1] = false;
|
||||
for ($i = 2; $i * $i <= $limit; $i++)
|
||||
if ($isP[$i]) for ($j = $i*$i; $j <= $limit; $j += $i) $isP[$j] = false;
|
||||
return array_sum(array_map('intval', $isP));
|
||||
}
|
||||
function rot13($s) { return str_rot13($s); }
|
||||
function caesar($s, $shift = 7) {
|
||||
$r = '';
|
||||
for ($i = 0; $i < strlen($s); $i++) {
|
||||
$c = $s[$i];
|
||||
if ($c >= 'a' && $c <= 'z') $r .= chr((ord($c) - 97 + $shift) % 26 + 97);
|
||||
elseif ($c >= 'A' && $c <= 'Z') $r .= chr((ord($c) - 65 + $shift) % 26 + 65);
|
||||
else $r .= $c;
|
||||
}
|
||||
return $r;
|
||||
}
|
||||
function matTrace($m) {
|
||||
$s = 0;
|
||||
foreach ($m as $i => $row) $s += $row[$i];
|
||||
return $s;
|
||||
}
|
||||
function palindrome($s) {
|
||||
$t = str_replace(' ', '', strtolower($s));
|
||||
return $t === strrev($t);
|
||||
}
|
||||
function checksumStr($data) { return substr(hash('sha256', $data), 0, 16); }
|
||||
function runningSum($arr) {
|
||||
$t = 0; $out = [];
|
||||
foreach ($arr as $v) { $t += $v; $out[] = $t; }
|
||||
return $out;
|
||||
}
|
||||
function countDistinct($arr) { return count(array_unique($arr)); }
|
||||
function groupByMod($arr, $n) {
|
||||
$g = array_fill(0, $n, 0);
|
||||
foreach ($arr as $v) $g[$v % $n]++;
|
||||
return $g;
|
||||
}
|
||||
function pascalRow($n) {
|
||||
$row = [1];
|
||||
for ($i = 1; $i <= $n; $i++) $row[] = intdiv($row[$i-1] * ($n-$i+1), $i);
|
||||
return $row;
|
||||
}
|
||||
function reverseWords($s) { return implode(' ', array_reverse(explode(' ', $s))); }
|
||||
function countVowels($s) { return preg_match_all('/[aeiou]/i', $s); }
|
||||
function isPrime($n) {
|
||||
if ($n < 2) return false;
|
||||
for ($i = 2; $i * $i <= $n; $i++) if ($n % $i === 0) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
function handler($ctx) {
|
||||
global $CONSTS;
|
||||
$mat = [[1,2,3,4],[5,6,7,8],[9,10,11,12],[13,14,15,16]];
|
||||
$data = range(1, 50);
|
||||
$constSum = 0; foreach ($CONSTS as $v) $constSum += $v;
|
||||
$rsum = runningSum($data);
|
||||
$result = [
|
||||
'tag' => 'big-code-php-ok',
|
||||
'fib_25' => fib(25),
|
||||
'primes_50' => sieve(50),
|
||||
'rot13' => rot13('Hello Fission'),
|
||||
'caesar' => caesar('heavy'),
|
||||
'const_sum' => round($constSum, 2),
|
||||
'mat_trace' => matTrace($mat),
|
||||
'palindrome' => palindrome('racecar'),
|
||||
'distinct' => countDistinct([1,2,2,3,3,3,4]),
|
||||
'checksum' => checksumStr('fission-heavy-test'),
|
||||
'rsum_last' => end($rsum),
|
||||
'pascal_4' => pascalRow(4),
|
||||
'rev_words' => reverseWords('big code php test'),
|
||||
'vowels' => countVowels('fission heavy test'),
|
||||
'is_prime_97'=> isPrime(97),
|
||||
'groups_3' => groupByMod(array_slice($data, 0, 30), 3),
|
||||
];
|
||||
$ctx["response"]->getBody()->write(json_encode($result));
|
||||
}
|
||||
PHPEOF
|
||||
create_json "php" "$FN_BIG_PHP" "${TMP}/big_php.php"
|
||||
|
||||
echo ""
|
||||
echo ">>> S1: invoke больших функций — ждём cold start всех 4 языков..."
|
||||
for fn_tag in \
|
||||
"${FN_BIG_PY}:big-code-py-ok:Python" \
|
||||
"${FN_BIG_NODE}:big-code-node-ok:Node.js" \
|
||||
"${FN_BIG_RUBY}:big-code-ruby-ok:Ruby" \
|
||||
"${FN_BIG_PHP}:big-code-php-ok:PHP"; do
|
||||
fn="${fn_tag%%:*}"; rest="${fn_tag#*:}"; tag="${rest%%:*}"; label="${rest#*:}"
|
||||
(
|
||||
if wait_invoke "$SUB" "$fn" "$tag" 20 6; then
|
||||
echo "OK"
|
||||
else
|
||||
echo "FAIL"
|
||||
fi
|
||||
) > "${TMP}/s1-${fn}.out" &
|
||||
done
|
||||
wait
|
||||
|
||||
for fn_tag in \
|
||||
"${FN_BIG_PY}:big-code-py-ok:Python" \
|
||||
"${FN_BIG_NODE}:big-code-node-ok:Node.js" \
|
||||
"${FN_BIG_RUBY}:big-code-ruby-ok:Ruby" \
|
||||
"${FN_BIG_PHP}:big-code-php-ok:PHP"; do
|
||||
fn="${fn_tag%%:*}"; rest="${fn_tag#*:}"; tag="${rest%%:*}"; label="${rest#*:}"
|
||||
result=$(cat "${TMP}/s1-${fn}.out" 2>/dev/null || echo "FAIL")
|
||||
if [ "$result" = "OK" ]; then
|
||||
pass "S1 $label BIG CODE invoke → 200"
|
||||
else
|
||||
fail "S1 $label BIG CODE invoke — не стабилизировался"
|
||||
fi
|
||||
done
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
# SECTION 2: CPU BURN — рекурсивный fib, нагрузка на CPU пода
|
||||
# Ищем: OOM kill, timeout, зависание пула, медленный poolmgr
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
echo ""
|
||||
echo "────────────────────────────────────────────────────────"
|
||||
echo " S2: CPU BURN — рекурсивный fib (Python/Node.js/Ruby/PHP)"
|
||||
echo "────────────────────────────────────────────────────────"
|
||||
|
||||
FN_CPU_PY="t-cpu-py-${RUN_ID}"
|
||||
cat > "${TMP}/cpu_py.py" << 'PYEOF'
|
||||
def fib(n):
|
||||
if n < 2: return n
|
||||
return fib(n-1) + fib(n-2)
|
||||
|
||||
def main(event, context):
|
||||
result = fib(28)
|
||||
return "cpu-burn-py:" + str(result), 200
|
||||
PYEOF
|
||||
create_json "python" "$FN_CPU_PY" "${TMP}/cpu_py.py"
|
||||
|
||||
FN_CPU_NODE="t-cpu-node-${RUN_ID}"
|
||||
cat > "${TMP}/cpu_node.js" << 'JSEOF'
|
||||
function fib(n) {
|
||||
if (n < 2) return n;
|
||||
return fib(n-1) + fib(n-2);
|
||||
}
|
||||
module.exports = async function(ctx) {
|
||||
return { status: 200, body: "cpu-burn-node:" + fib(35) };
|
||||
};
|
||||
JSEOF
|
||||
create_json "nodejs" "$FN_CPU_NODE" "${TMP}/cpu_node.js"
|
||||
|
||||
FN_CPU_RUBY="t-cpu-ruby-${RUN_ID}"
|
||||
cat > "${TMP}/cpu_ruby.rb" << 'RBEOF'
|
||||
def fib(n)
|
||||
return n if n < 2
|
||||
fib(n-1) + fib(n-2)
|
||||
end
|
||||
|
||||
def handler(ctx)
|
||||
{ statusCode: 200, body: "cpu-burn-ruby:#{fib(25)}" }
|
||||
end
|
||||
RBEOF
|
||||
create_json "ruby" "$FN_CPU_RUBY" "${TMP}/cpu_ruby.rb"
|
||||
|
||||
FN_CPU_PHP="t-cpu-php-${RUN_ID}"
|
||||
cat > "${TMP}/cpu_php.php" << 'PHPEOF'
|
||||
<?php
|
||||
function fib($n) {
|
||||
if ($n < 2) return $n;
|
||||
return fib($n-1) + fib($n-2);
|
||||
}
|
||||
function handler($ctx) {
|
||||
$ctx["response"]->getBody()->write('cpu-burn-php:' . fib(28));
|
||||
}
|
||||
PHPEOF
|
||||
create_json "php" "$FN_CPU_PHP" "${TMP}/cpu_php.php"
|
||||
|
||||
echo ""
|
||||
echo ">>> S2: invoke CPU-burn функций..."
|
||||
for fn_tag in \
|
||||
"${FN_CPU_PY}:cpu-burn-py:Python" \
|
||||
"${FN_CPU_NODE}:cpu-burn-node:Node.js" \
|
||||
"${FN_CPU_RUBY}:cpu-burn-ruby:Ruby" \
|
||||
"${FN_CPU_PHP}:cpu-burn-php:PHP"; do
|
||||
fn="${fn_tag%%:*}"; rest="${fn_tag#*:}"; tag="${rest%%:*}"; label="${rest#*:}"
|
||||
(
|
||||
if wait_invoke "$SUB" "$fn" "$tag" 18 6; then
|
||||
echo "OK"
|
||||
else
|
||||
echo "FAIL"
|
||||
fi
|
||||
) > "${TMP}/s2-${fn}.out" &
|
||||
done
|
||||
wait
|
||||
|
||||
for fn_tag in \
|
||||
"${FN_CPU_PY}:cpu-burn-py:Python" \
|
||||
"${FN_CPU_NODE}:cpu-burn-node:Node.js" \
|
||||
"${FN_CPU_RUBY}:cpu-burn-ruby:Ruby" \
|
||||
"${FN_CPU_PHP}:cpu-burn-php:PHP"; do
|
||||
fn="${fn_tag%%:*}"; rest="${fn_tag#*:}"; tag="${rest%%:*}"; label="${rest#*:}"
|
||||
result=$(cat "${TMP}/s2-${fn}.out" 2>/dev/null || echo "FAIL")
|
||||
if [ "$result" = "OK" ]; then
|
||||
pass "S2 $label CPU BURN invoke → 200"
|
||||
else
|
||||
fail "S2 $label CPU BURN — не стабилизировался"
|
||||
fi
|
||||
done
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
# SECTION 3: БОЛЬШОЙ ОТВЕТ — 50KB body
|
||||
# Ищем: truncation, proxy memory issues, timeout на больших ответах
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
echo ""
|
||||
echo "────────────────────────────────────────────────────────"
|
||||
echo " S3: БОЛЬШОЙ ОТВЕТ — 50KB (Python + Node.js)"
|
||||
echo "────────────────────────────────────────────────────────"
|
||||
|
||||
FN_RESP_PY="t-resp-py-${RUN_ID}"
|
||||
cat > "${TMP}/resp_py.py" << 'PYEOF'
|
||||
def main(event, context):
|
||||
chunk = "FISSION-HEAVY-DATA-" * 100 # 1900 chars
|
||||
data = (chunk * 27)[:50000] # ровно 50000 chars
|
||||
return "bigresponse-py-ok:" + data[:20] + "..." + str(len(data)), 200
|
||||
PYEOF
|
||||
create_json "python" "$FN_RESP_PY" "${TMP}/resp_py.py"
|
||||
|
||||
FN_RESP_NODE="t-resp-node-${RUN_ID}"
|
||||
cat > "${TMP}/resp_node.js" << 'JSEOF'
|
||||
module.exports = async function(ctx) {
|
||||
const chunk = "FISSION-HEAVY-DATA-".repeat(100);
|
||||
const data = (chunk.repeat(27)).slice(0, 50000);
|
||||
return { status: 200, body: "bigresponse-node-ok:" + data.slice(0,20) + "..." + data.length };
|
||||
};
|
||||
JSEOF
|
||||
create_json "nodejs" "$FN_RESP_NODE" "${TMP}/resp_node.js"
|
||||
|
||||
echo ""
|
||||
echo ">>> S3: invoke и проверка размера ответа..."
|
||||
for fn_tag in \
|
||||
"${FN_RESP_PY}:bigresponse-py-ok:Python" \
|
||||
"${FN_RESP_NODE}:bigresponse-node-ok:Node.js"; do
|
||||
fn="${fn_tag%%:*}"; rest="${fn_tag#*:}"; tag="${rest%%:*}"; label="${rest#*:}"
|
||||
if wait_invoke "$SUB" "$fn" "$tag" 15 6; then
|
||||
# Проверяем реальный размер ответа
|
||||
RESP=$(curl -s --max-time 60 -X POST "${BASE}/functions/${fn}/invoke" \
|
||||
-H "X-Auth-Token: ${SUB}" -H "Content-Type: application/json" -d '{}')
|
||||
RAW=$(json_field "$RESP" response_raw)
|
||||
RAW_LEN=${#RAW}
|
||||
echo " response_raw length = $RAW_LEN chars"
|
||||
if [ "$RAW_LEN" -gt 40000 ]; then
|
||||
pass "S3 $label BIG RESPONSE size=$RAW_LEN (>40000)"
|
||||
else
|
||||
fail "S3 $label BIG RESPONSE слишком мал: size=$RAW_LEN"
|
||||
fi
|
||||
else
|
||||
fail "S3 $label BIG RESPONSE — invoke не стабилизировался"
|
||||
fi
|
||||
done
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
# SECTION 4: АРХИВЫ ИЗ ZIP — создаём архивы на лету, все 4 языка
|
||||
# Ищем: баги пути upload vs inline, entrypoint defaults, storagesvc PV
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
echo ""
|
||||
echo "────────────────────────────────────────────────────────"
|
||||
echo " S4: АРХИВ ИЗ ZIP (Python / Node.js / Ruby / PHP)"
|
||||
echo "────────────────────────────────────────────────────────"
|
||||
|
||||
# --- Создаём ZIP-архивы на лету через python3 ---
|
||||
|
||||
# Python archive: main.py (entrypoint по умолчанию: main.main)
|
||||
python3 - > "${TMP}/arc-py.zip" << 'EOF'
|
||||
import zipfile, io, sys
|
||||
buf = io.BytesIO()
|
||||
code = """import json
|
||||
def main(event, context):
|
||||
return json.dumps({"tag": "archive-py-ok", "fib25": 75025}), 200
|
||||
"""
|
||||
with zipfile.ZipFile(buf, 'w', zipfile.ZIP_DEFLATED) as zf:
|
||||
zf.writestr('main.py', code)
|
||||
sys.stdout.buffer.write(buf.getvalue())
|
||||
EOF
|
||||
|
||||
# Node.js archive: main.js (entrypoint: main → файл main.js)
|
||||
python3 - > "${TMP}/arc-node.zip" << 'EOF'
|
||||
import zipfile, io, sys
|
||||
buf = io.BytesIO()
|
||||
code = """module.exports = async function(ctx) {
|
||||
return { status: 200, body: JSON.stringify({ tag: "archive-node-ok", fib25: 75025 }) };
|
||||
};
|
||||
"""
|
||||
pkg = '{"name":"heavy-node","version":"1.0.0"}'
|
||||
with zipfile.ZipFile(buf, 'w', zipfile.ZIP_DEFLATED) as zf:
|
||||
zf.writestr('main.js', code)
|
||||
zf.writestr('package.json', pkg)
|
||||
sys.stdout.buffer.write(buf.getvalue())
|
||||
EOF
|
||||
|
||||
# Ruby archive: handler.rb (entrypoint: handler)
|
||||
python3 - > "${TMP}/arc-ruby.zip" << 'EOF'
|
||||
import zipfile, io, sys
|
||||
buf = io.BytesIO()
|
||||
code = """require 'json'
|
||||
def handler(ctx)
|
||||
{ statusCode: 200, body: JSON.generate({ tag: "archive-ruby-ok", fib25: 75025 }) }
|
||||
end
|
||||
"""
|
||||
with zipfile.ZipFile(buf, 'w', zipfile.ZIP_DEFLATED) as zf:
|
||||
zf.writestr('handler.rb', code)
|
||||
sys.stdout.buffer.write(buf.getvalue())
|
||||
EOF
|
||||
|
||||
# PHP archive: main.php (entrypoint: main.php::handler)
|
||||
python3 - > "${TMP}/arc-php.zip" << 'EOF'
|
||||
import zipfile, io, sys
|
||||
buf = io.BytesIO()
|
||||
code = """<?php
|
||||
function handler($ctx) {
|
||||
$ctx[\"response\"]->getBody()->write(json_encode(['tag' => 'archive-php-ok', 'fib25' => 75025]));
|
||||
}
|
||||
"""
|
||||
with zipfile.ZipFile(buf, 'w', zipfile.ZIP_DEFLATED) as zf:
|
||||
zf.writestr('main.php', code)
|
||||
sys.stdout.buffer.write(buf.getvalue())
|
||||
EOF
|
||||
|
||||
# Загружаем архивы через multipart POST /functions
|
||||
declare -A ARC_LANGS=( ["py"]="python" ["node"]="nodejs" ["ruby"]="ruby" ["php"]="php" )
|
||||
declare -A ARC_TAGS=( ["py"]="archive-py-ok" ["node"]="archive-node-ok" ["ruby"]="archive-ruby-ok" ["php"]="archive-php-ok" )
|
||||
|
||||
for lang_short in py node ruby php; do
|
||||
fn="t-arc-${lang_short}-${RUN_ID}"
|
||||
reg "$fn"
|
||||
lang="${ARC_LANGS[$lang_short]}"
|
||||
arcfile="${TMP}/arc-${lang_short}.zip"
|
||||
|
||||
R=$(curl -s -w "\n%{http_code}" -X POST "${BASE}/functions" \
|
||||
-H "X-Auth-Token: ${SUB}" \
|
||||
-F "name=${fn}" \
|
||||
-F "language=${lang}" \
|
||||
-F "archive=@${arcfile};type=application/zip" 2>/dev/null)
|
||||
CODE=$(echo "$R" | tail -1)
|
||||
BODY=$(echo "$R" | head -n -1)
|
||||
check_http "$CODE" "201" "S4 ${lang_short^^} ARCHIVE upload → 201"
|
||||
if [ "$CODE" != "201" ]; then
|
||||
echo " upload error: $(printf '%s' "$BODY" | head -c 200)"
|
||||
fi
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo ">>> S4: invoke архивных функций — ждём cold start..."
|
||||
for lang_short in py node ruby php; do
|
||||
fn="t-arc-${lang_short}-${RUN_ID}"
|
||||
tag="${ARC_TAGS[$lang_short]}"
|
||||
(
|
||||
if wait_invoke "$SUB" "$fn" "$tag" 20 6; then
|
||||
echo "OK"
|
||||
else
|
||||
echo "FAIL"
|
||||
fi
|
||||
) > "${TMP}/s4-${fn}.out" &
|
||||
done
|
||||
wait
|
||||
|
||||
for lang_short in py node ruby php; do
|
||||
fn="t-arc-${lang_short}-${RUN_ID}"
|
||||
tag="${ARC_TAGS[$lang_short]}"
|
||||
result=$(cat "${TMP}/s4-${fn}.out" 2>/dev/null || echo "FAIL")
|
||||
if [ "$result" = "OK" ]; then
|
||||
pass "S4 ${lang_short^^} ARCHIVE invoke → 200"
|
||||
else
|
||||
fail "S4 ${lang_short^^} ARCHIVE invoke — не стабилизировался"
|
||||
fi
|
||||
done
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
# SECTION 5: БЫСТРЫЕ СЕРИИ — 12 последовательных invoke одной функции
|
||||
# Ищем: poolmgr reuse bugs, утечки памяти, деградацию latency
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
echo ""
|
||||
echo "────────────────────────────────────────────────────────"
|
||||
echo " S5: БЫСТРЫЕ СЕРИИ — 12x invoke подряд (Python)"
|
||||
echo "────────────────────────────────────────────────────────"
|
||||
|
||||
FN_RAPID="t-rapid-py-${RUN_ID}"
|
||||
cat > "${TMP}/rapid_py.py" << 'PYEOF'
|
||||
import time
|
||||
_count = 0
|
||||
def main(event, context):
|
||||
global _count
|
||||
_count += 1
|
||||
return "rapid-ok:{}".format(_count), 200
|
||||
PYEOF
|
||||
create_json "python" "$FN_RAPID" "${TMP}/rapid_py.py"
|
||||
|
||||
echo ">>> Ждём cold start..."
|
||||
if wait_invoke "$SUB" "$FN_RAPID" "rapid-ok" 15 5; then
|
||||
echo ">>> S5: Запускаем 12 последовательных invoke..."
|
||||
RAPID_OK=0
|
||||
RAPID_FAIL=0
|
||||
LATENCIES=()
|
||||
for i in $(seq 1 12); do
|
||||
T_START=$(date +%s%3N)
|
||||
RESP=$(curl -s --max-time 20 -X POST "${BASE}/functions/${FN_RAPID}/invoke" \
|
||||
-H "X-Auth-Token: ${SUB}" -H "Content-Type: application/json" -d '{}')
|
||||
T_END=$(date +%s%3N)
|
||||
LAT=$(( T_END - T_START ))
|
||||
STATUS=$(json_field "$RESP" status)
|
||||
RAW=$(json_field "$RESP" response_raw)
|
||||
echo " invoke ${i}/12: status=$STATUS raw=$(printf '%s' "$RAW" | head -c 40) latency=${LAT}ms"
|
||||
if [ "$STATUS" = "200" ]; then
|
||||
RAPID_OK=$((RAPID_OK+1))
|
||||
LATENCIES+=("$LAT")
|
||||
else
|
||||
RAPID_FAIL=$((RAPID_FAIL+1))
|
||||
fi
|
||||
done
|
||||
[ "$RAPID_FAIL" -eq 0 ] && pass "S5 RAPID FIRE: 12/12 invoke → 200" \
|
||||
|| fail "S5 RAPID FIRE: ${RAPID_FAIL}/12 упали"
|
||||
# Статистика latency
|
||||
if [ "${#LATENCIES[@]}" -gt 0 ]; then
|
||||
LAT_MIN="${LATENCIES[0]}"; LAT_MAX="${LATENCIES[0]}"
|
||||
for lat in "${LATENCIES[@]}"; do
|
||||
[ "$lat" -lt "$LAT_MIN" ] && LAT_MIN="$lat"
|
||||
[ "$lat" -gt "$LAT_MAX" ] && LAT_MAX="$lat"
|
||||
done
|
||||
echo " latency min=${LAT_MIN}ms max=${LAT_MAX}ms"
|
||||
fi
|
||||
else
|
||||
fail "S5 RAPID FIRE — cold start не прошёл, тест пропущен"
|
||||
fi
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
# SECTION 6: ПАРАЛЛЕЛЬНЫЕ ВЫЗОВЫ — 5 параллельных invoke на каждый язык
|
||||
# Ищем: race conditions, конфликты при pod scheduling, deadlocks
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
echo ""
|
||||
echo "────────────────────────────────────────────────────────"
|
||||
echo " S6: ПАРАЛЛЕЛЬНЫЕ ВЫЗОВЫ — 5 одновременных invoke × 4 языка"
|
||||
echo "────────────────────────────────────────────────────────"
|
||||
PARALLEL_N=5
|
||||
|
||||
# Используем уже тёплые функции из S2 (CPU burn — уже invoke'd)
|
||||
declare -A PAR_FNS=( ["Python"]="$FN_CPU_PY" ["Node.js"]="$FN_CPU_NODE" ["Ruby"]="$FN_CPU_RUBY" ["PHP"]="$FN_CPU_PHP" )
|
||||
declare -A PAR_TAGS=( ["Python"]="cpu-burn-py" ["Node.js"]="cpu-burn-node" ["Ruby"]="cpu-burn-ruby" ["PHP"]="cpu-burn-php" )
|
||||
|
||||
for lang in "Python" "Node.js" "Ruby" "PHP"; do
|
||||
fn="${PAR_FNS[$lang]}"
|
||||
tag="${PAR_TAGS[$lang]}"
|
||||
echo ""
|
||||
echo ">>> S6 $lang: $PARALLEL_N параллельных invoke..."
|
||||
for i in $(seq 1 "$PARALLEL_N"); do
|
||||
(
|
||||
RESP=$(curl -s --max-time 60 -X POST "${BASE}/functions/${fn}/invoke" \
|
||||
-H "X-Auth-Token: ${SUB}" -H "Content-Type: application/json" -d '{}')
|
||||
STATUS=$(json_field "$RESP" status)
|
||||
RAW=$(json_field "$RESP" response_raw)
|
||||
if [ "$STATUS" = "200" ] && printf '%s' "$RAW" | grep -qF "$tag"; then
|
||||
echo "OK" > "${TMP}/par-${lang}-${i}.out"
|
||||
else
|
||||
echo "FAIL:status=${STATUS}" > "${TMP}/par-${lang}-${i}.out"
|
||||
fi
|
||||
) &
|
||||
done
|
||||
wait
|
||||
|
||||
PAR_OK=0; PAR_FAIL=0
|
||||
for i in $(seq 1 "$PARALLEL_N"); do
|
||||
result=$(cat "${TMP}/par-${lang}-${i}.out" 2>/dev/null || echo "FAIL:no-file")
|
||||
if [ "$result" = "OK" ]; then
|
||||
PAR_OK=$((PAR_OK+1))
|
||||
else
|
||||
PAR_FAIL=$((PAR_FAIL+1))
|
||||
echo " invoke $i: $result"
|
||||
fi
|
||||
done
|
||||
[ "$PAR_FAIL" -eq 0 ] \
|
||||
&& pass "S6 $lang PARALLEL: ${PARALLEL_N}/${PARALLEL_N} invoke → 200" \
|
||||
|| fail "S6 $lang PARALLEL: ${PAR_FAIL}/${PARALLEL_N} упали"
|
||||
done
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
# SECTION 7: МЕДЛЕННЫЕ ФУНКЦИИ — sleep 6s, Python + Node.js
|
||||
# Ищем: router timeout, poolmgr idle eviction, 504 под нагрузкой
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
echo ""
|
||||
echo "────────────────────────────────────────────────────────"
|
||||
echo " S7: МЕДЛЕННЫЕ ФУНКЦИИ — sleep(6s) × Python + Node.js"
|
||||
echo "────────────────────────────────────────────────────────"
|
||||
|
||||
FN_SLOW_PY="t-slow-py-${RUN_ID}"
|
||||
cat > "${TMP}/slow_py.py" << 'PYEOF'
|
||||
import time
|
||||
def main(event, context):
|
||||
time.sleep(6)
|
||||
return "slow-py-ok", 200
|
||||
PYEOF
|
||||
create_json "python" "$FN_SLOW_PY" "${TMP}/slow_py.py"
|
||||
|
||||
FN_SLOW_NODE="t-slow-node-${RUN_ID}"
|
||||
cat > "${TMP}/slow_node.js" << 'JSEOF'
|
||||
module.exports = async function(ctx) {
|
||||
await new Promise(r => setTimeout(r, 6000));
|
||||
return { status: 200, body: "slow-node-ok" };
|
||||
};
|
||||
JSEOF
|
||||
create_json "nodejs" "$FN_SLOW_NODE" "${TMP}/slow_node.js"
|
||||
|
||||
echo ""
|
||||
echo ">>> S7: invoke (ждём cold start + 6s sleep)..."
|
||||
for fn_tag in \
|
||||
"${FN_SLOW_PY}:slow-py-ok:Python" \
|
||||
"${FN_SLOW_NODE}:slow-node-ok:Node.js"; do
|
||||
fn="${fn_tag%%:*}"; rest="${fn_tag#*:}"; tag="${rest%%:*}"; label="${rest#*:}"
|
||||
T_START=$(date +%s%3N)
|
||||
if wait_invoke "$SUB" "$fn" "$tag" 15 8; then
|
||||
T_END=$(date +%s%3N)
|
||||
LAT=$(( T_END - T_START ))
|
||||
pass "S7 $label SLOW(6s) → 200 за ~${LAT}ms total"
|
||||
# Повторный invoke (warm pod): должно занять ≥6s
|
||||
T2=$(date +%s%3N)
|
||||
RESP=$(curl -s --max-time 30 -X POST "${BASE}/functions/${fn}/invoke" \
|
||||
-H "X-Auth-Token: ${SUB}" -H "Content-Type: application/json" -d '{}')
|
||||
T2E=$(date +%s%3N)
|
||||
LAT2=$(( T2E - T2 ))
|
||||
STATUS=$(json_field "$RESP" status)
|
||||
RAW=$(json_field "$RESP" response_raw)
|
||||
echo " warm invoke: status=$STATUS latency=${LAT2}ms raw=$(printf '%s' "$RAW" | head -c 40)"
|
||||
if [ "$STATUS" = "200" ] && [ "$LAT2" -ge 5000 ]; then
|
||||
pass "S7 $label SLOW warm invoke → 200 latency=${LAT2}ms (≥5s)"
|
||||
elif [ "$STATUS" = "200" ]; then
|
||||
fail "S7 $label SLOW warm слишком быстро: ${LAT2}ms — sleep не сработал?"
|
||||
else
|
||||
fail "S7 $label SLOW warm invoke → status=$STATUS"
|
||||
fi
|
||||
else
|
||||
fail "S7 $label SLOW — cold start не прошёл"
|
||||
fi
|
||||
done
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
# SECTION 8: ДАВЛЕНИЕ ПО ПАМЯТИ — аллокация 20MB, Python + Node.js
|
||||
# Ищем: OOM kill на поде, зависание, некорректный ответ
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
echo ""
|
||||
echo "────────────────────────────────────────────────────────"
|
||||
echo " S8: ПАМЯТЬ — аллокация 20MB (Python + Node.js)"
|
||||
echo "────────────────────────────────────────────────────────"
|
||||
|
||||
FN_MEM_PY="t-mem-py-${RUN_ID}"
|
||||
cat > "${TMP}/mem_py.py" << 'PYEOF'
|
||||
def main(event, context):
|
||||
# Аллоцируем 20MB и суммируем (чтобы не оптимизировалось)
|
||||
data = bytearray(20 * 1024 * 1024)
|
||||
for i in range(0, len(data), 4096):
|
||||
data[i] = i % 256
|
||||
checksum = sum(data[i] for i in range(0, len(data), 4096)) % 65536
|
||||
return "mem-py-ok:{}:{}".format(len(data), checksum), 200
|
||||
PYEOF
|
||||
create_json "python" "$FN_MEM_PY" "${TMP}/mem_py.py"
|
||||
|
||||
FN_MEM_NODE="t-mem-node-${RUN_ID}"
|
||||
cat > "${TMP}/mem_node.js" << 'JSEOF'
|
||||
module.exports = async function(ctx) {
|
||||
const size = 20 * 1024 * 1024;
|
||||
const buf = Buffer.alloc(size);
|
||||
for (let i = 0; i < size; i += 4096) buf[i] = i % 256;
|
||||
let checksum = 0;
|
||||
for (let i = 0; i < size; i += 4096) checksum = (checksum + buf[i]) % 65536;
|
||||
return { status: 200, body: `mem-node-ok:${size}:${checksum}` };
|
||||
};
|
||||
JSEOF
|
||||
create_json "nodejs" "$FN_MEM_NODE" "${TMP}/mem_node.js"
|
||||
|
||||
echo ""
|
||||
echo ">>> S8: invoke (20MB аллокация)..."
|
||||
for fn_tag in \
|
||||
"${FN_MEM_PY}:mem-py-ok:Python" \
|
||||
"${FN_MEM_NODE}:mem-node-ok:Node.js"; do
|
||||
fn="${fn_tag%%:*}"; rest="${fn_tag#*:}"; tag="${rest%%:*}"; label="${rest#*:}"
|
||||
if wait_invoke "$SUB" "$fn" "$tag" 18 6; then
|
||||
pass "S8 $label MEMORY(20MB) → 200"
|
||||
# Проверяем что данные в ответе правильные
|
||||
RESP=$(curl -s --max-time 30 -X POST "${BASE}/functions/${fn}/invoke" \
|
||||
-H "X-Auth-Token: ${SUB}" -H "Content-Type: application/json" -d '{}')
|
||||
RAW=$(json_field "$RESP" response_raw)
|
||||
if printf '%s' "$RAW" | grep -qE "$tag:[0-9]+:[0-9]+"; then
|
||||
pass "S8 $label MEMORY ответ содержит checksum: $(printf '%s' "$RAW" | head -c 60)"
|
||||
else
|
||||
fail "S8 $label MEMORY ответ некорректен: $(printf '%s' "$RAW" | head -c 60)"
|
||||
fi
|
||||
else
|
||||
fail "S8 $label MEMORY(20MB) — не вернулась (OOM kill?)"
|
||||
fi
|
||||
done
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
print_summary
|
||||
Reference in New Issue
Block a user