Compare commits
8
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
27dbe3bccb | ||
|
|
090a7afdd7 | ||
|
|
042b1e5046 | ||
|
|
72e5d50cd0 | ||
|
|
fd075a7f80 | ||
|
|
78f1a2cfc4 | ||
|
|
2aadae339e | ||
|
|
3eb4f5669b |
@@ -0,0 +1,87 @@
|
||||
# Образы нашего Fission — актуальные теги
|
||||
|
||||
> Обновлено: 2026-05-19
|
||||
|
||||
## Текущие образы (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.94` |
|
||||
| **Python environment** | `naeel/fission-python-env` | `v1.1` |
|
||||
| **metrics-collector** | `naeel/metrics-collector` | `v0.1.3` |
|
||||
|
||||
---
|
||||
|
||||
## Где в коде консоли прописаны образы
|
||||
|
||||
### 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` → при смене образа **всегда** менять тег, не переиспользовать старый
|
||||
@@ -58,7 +58,7 @@ spec:
|
||||
serviceAccountName: fission-console
|
||||
containers:
|
||||
- name: console
|
||||
image: naeel/fission-console:v1.3.92
|
||||
image: naeel/fission-console:v1.3.94
|
||||
imagePullPolicy: Always
|
||||
ports:
|
||||
- containerPort: 8090
|
||||
|
||||
@@ -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 {
|
||||
@@ -402,7 +412,7 @@ func (m *NSManager) reapExpiredFunctionsInNS(ctx context.Context, ns string, now
|
||||
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
|
||||
|
||||
@@ -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.94</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.94</span>
|
||||
<button class="btn ghost" onclick="closeHelp()">Закрыть</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,43 @@
|
||||
# Progress Log
|
||||
|
||||
## 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,30 @@
|
||||
# Fission helm values — PV (local storage) backend
|
||||
# Применять: helm upgrade fission fission-charts/fission-all --version 1.22.1 \
|
||||
# -n fission -f helm/fission-values.yaml
|
||||
#
|
||||
# Секретов нет — local storage не требует credentials.
|
||||
# При деплое в новый кластер — только этот файл.
|
||||
|
||||
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 # сетевой block-device VMware Cloud Director (надёжнее local-path)
|
||||
size: 2Gi
|
||||
accessMode: ReadWriteOnce
|
||||
|
||||
repository: ""
|
||||
routerServiceType: ClusterIP
|
||||
serviceType: ClusterIP
|
||||
|
||||
analytics: false
|
||||
@@ -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