diff --git a/doc/decisions/log.md b/doc/decisions/log.md index 771ab7c..8b494f3 100644 --- a/doc/decisions/log.md +++ b/doc/decisions/log.md @@ -861,3 +861,72 @@ for _, k := range keys { envVars = append(envVars, corev1.EnvVar{Name: k, Value: **Тесты:** 2 теста в `controllers/function_controller_unit_test.go` (4 env vars → алфавитный порядок после SLESS_ENTRYPOINT; пустой Env → только SLESS_ENTRYPOINT). + +--- + +## 2026-03-19 — pgx/v5 как PG-драйвер для Go функций (vs database/sql + lib/pq) + +**Контекст:** Go runtime v0.1.1 — добавляем прямой доступ к PostgreSQL из функций. +Нужно выбрать: database/sql + lib/pq, или чистый pgx/v5? + +**Решение:** Использовать `github.com/jackc/pgx/v5` напрямую, без обёртки database/sql. + +**Причины:** + +1. **pgxpool из коробки** — `pgxpool.New()` без дополнительных пакетов. lib/pq требует `sql.Open` + настройку пула через `db.SetMaxOpenConns` и т.д. + +2. **Нативный протокол PostgreSQL** — pgx реализует wire protocol напрямую, без CGO. + lib/pq тоже pure Go, но pgx быстрее (~20% в бенчмарках) и активнее поддерживается. + +3. **Контекст-нативность** — `pgxpool.Pool.Query(ctx, ...)` — context как первый аргумент везде. + В database/sql контекст пришёл только в Go 1.8 как `QueryContext` — неудобный retrofit. + +4. **Сканирование строк** — `pgx.CollectRows`, `pgx.ForEachRow` — удобнее чем `rows.Scan`. + +5. **Экосистема** — pgx — де-факто стандарт в Go+PG проектах (используется в pgx, pgvector, ent). + +**Что добавлено в рантайм:** +``` +github.com/jackc/pgx/v5 v5.7.2 +github.com/jackc/pgpassfile v1.0.0 // indirect +github.com/jackc/pgservicefile v0.0.0-... // indirect +github.com/jackc/puddle/v2 v2.2.2 // indirect (connection pool) +golang.org/x/crypto v0.31.0 // indirect (scram auth) +golang.org/x/sync v0.10.0 // indirect +golang.org/x/text v0.21.0 // indirect +``` + +**go mod download** добавлен в Dockerfile до COPY server.go — слой с зависимостями кешируется отдельно. +Пересборка функции (только изменение handler.go) не перекачивает ~15MB зависимостей. + +--- + +## 2026-03-19 — Динамический таймаут в invoke.go из Function.Spec.TimeoutSec + +**Контекст:** invoke.go проксирует HTTP-запросы к подам функций. До этого — глобальный `http.Client{Timeout: 30s}`. + +**Проблема:** 30s — константа времени написания кода. Функции с `timeout_sec=700` (stress-тесты, batch-задачи) падают с `context deadline exceeded` раньше чем успевают завершиться. + +**Решение:** Перед каждым вызовом читать `Function.Spec.TimeoutSec` из k8s и создавать `http.Client` с таймаутом = `TimeoutSec + 5s`. + +**Почему +5s буфер:** +- Нельзя ставить ровно `TimeoutSec` — есть сетевые задержки, TLS handshake, время на DNS резолв внутри кластера. +- 5s достаточно для любых сетевых задержек в локальном k8s кластере. +- Если функция реально завис на TimeoutSec — runtime сам должен прервать работу (это ответственность функции, не прокси). + +**Почему не кешировать http.Client:** +- Каждый вызов может прийти к разной функции с разным TimeoutSec. +- http.Client создаётся дёшево — только структура с одним полем Timeout. +- Кеш потребовал бы sync.Map или mutex — лишняя сложность без измеримой пользы. + +**Деградация при недоступности k8s:** +```go +if err := h.K8s.Get(r.Context(), client.ObjectKey{...}, fn); err == nil { + timeoutSec = fn.Spec.TimeoutSec +} +// если Get упал — timeoutSec=0 → invokeHTTPClient вернёт 30s (дефолт) +``` +Это осознанный выбор: если мы не можем прочитать функцию — мы не знаем её таймаут, +используем разумный дефолт вместо возврата ошибки. + +**Коммит:** `d7fda15`, оператор `v0.1.40` diff --git a/doc/errors/log.md b/doc/errors/log.md index e924a7f..303b221 100644 --- a/doc/errors/log.md +++ b/doc/errors/log.md @@ -631,3 +631,127 @@ Attribute runtime value must be one of: ["nodejs20" "python3.11" "go1.21"], got: ### Версии - Оператор: `naeel/sless-operator:v0.1.13` - Провайдер: `terra.k8c.ru/naeel/sless v0.1.7` + +--- + +## 2026-03-19 — Баг 4: Хардкодный 30s таймаут в invoke.go → context deadline exceeded + +### Симптом + +Вызов функции `stress-go-pgstorm` с `duration_sec=30` возвращал: +```json +{"error": "function unreachable: ... context deadline exceeded"} +``` +При этом под был `Running`, логи показывали нормальную работу pgxpool. + +### Корневая причина + +В `internal/api/handler/invoke.go` (строка 24) был глобальный http.Client: +```go +var httpClient = &http.Client{Timeout: 30 * time.Second} +``` +Функция реально отрабатывала ровно 30 секунд (duration_sec=30) + накладные расходы +на pgxpool.New() и первый коннект к БД ≈ 1-2 секунды. +Итого запрос превышал 30s → оператор разрывал соединение раньше чем функция успевала ответить. + +### Почему так было написано + +При создании invoke.go в марте 2026 таймаут 30s считался "достаточным для холодного +старта". Длительные функции тогда не планировались. Когда появились batch/stress задачи +с timeout_sec=600-700 — баг стал критическим. + +### Решение + +Убрать глобальный `httpClient`. Перед каждым вызовом: +1. Получить Function CRD из k8s: `h.K8s.Get(ctx, ObjectKey{name, ns}, fn)` +2. Прочитать `fn.Spec.TimeoutSec` +3. Создать `http.Client{Timeout: TimeoutSec*time.Second + 5*time.Second}` +4. Если функция не найдена (Get вернул ошибку) — дефолт 30s + +```go +func invokeHTTPClient(timeoutSec int32) *http.Client { + t := time.Duration(timeoutSec)*time.Second + 5*time.Second + if timeoutSec <= 0 { + t = 30 * time.Second + } + return &http.Client{Timeout: t} +} +``` + +### Файл + +`internal/api/handler/invoke.go` — исправлено в коммите `d7fda15` +Оператор пересобран: `naeel/sless-operator:v0.1.40` + +### Урок + +**Никогда не хардкодить таймауты** в прокси-слое. Таймаут всегда должен браться +из конфигурации вызываемого ресурса. `Function.Spec.TimeoutSec` существует именно для этого. + +--- + +## 2026-03-19 — Баг 5: nginx ingress proxy-read-timeout не задан → 504 Gateway Time-out + +### Симптом + +После фикса invoke.go (баг 4) — повторный вызов `stress-go-pgstorm` вернул: +```html +504 Gateway Time-out +

504 Gateway Time-out

+
nginx
+``` +curl exit code 5 (не 0), процесс завершился через ~60 секунд после старта запроса. + +### Корневая причина + +У ingress `sless-operator` не было аннотации `proxy-read-timeout`. +nginx ingress controller использует дефолт **60 секунд** если аннотация отсутствует. + +```yaml +# Было — аннотаций timeout нет вообще: +annotations: + kubernetes.io/ingress.class: nginx + nginx.ingress.kubernetes.io/force-ssl-redirect: "true" + nginx.ingress.kubernetes.io/ssl-redirect: "true" +``` + +Цепочка: клиент → nginx (60s timeout) → оператор (705s) → функция (600s). +Nginx оборвал соединение на 60-й секунде, хотя и оператор и функция были живы. + +### Диагностика + +Проверили аннотации всех ingress в ns sless: +- `nodered` ingress: `proxy-read-timeout: "3600"` ✅ (кто-то правильно настроил) +- `sless-funcs-ingress`: нет timeout аннотаций +- `sless-operator`: нет timeout аннотаций ← **виновник** + +### Решение + +1. `kubectl annotate` для мгновенного применения: +```bash +kubectl annotate ingress sless-operator -n sless \ + nginx.ingress.kubernetes.io/proxy-read-timeout="900" \ + nginx.ingress.kubernetes.io/proxy-send-timeout="900" --overwrite +``` + +2. Сохранить в манифест `deployments/k8s/operator.yaml`: +```yaml +nginx.ingress.kubernetes.io/proxy-read-timeout: "900" +nginx.ingress.kubernetes.io/proxy-send-timeout: "900" +``` + +### Почему 900s + +- function timeout_sec = 700 → оператор ждёт 705s +- nginx должен ждать дольше чем оператор → 900s с запасом +- Не ставим 3600s как у nodered — избыточно для функций + +### Файл + +`deployments/k8s/operator.yaml` — обновлено в коммите `d7fda15` + +### Урок + +При развёртывании нового ingress **всегда явно задавать** `proxy-read-timeout` +и `proxy-send-timeout`. Nginx дефолт 60s подходит только для быстрых API. +Для любых операций дольше 30s — обязательны явные таймауты. diff --git a/doc/progress.md b/doc/progress.md index d620f14..bb73cf3 100644 --- a/doc/progress.md +++ b/doc/progress.md @@ -1,45 +1,66 @@ # Прогресс разработки -Последнее обновление: 2026-03-19 21:00 +Последнее обновление: 2026-03-19 22:30 --- -## 2026-03-19 — Go runtime v0.1.1: pgx/v5 + stress-go-pgstorm ⏳ В РАБОТЕ +## 2026-03-19 — Go runtime v0.1.1: pgx/v5 + stress-go-pgstorm ✅ ЗАВЕРШЕНО ### Цель -Новая функция `stress-go-pgstorm` — Go код со 100 горутинами, которые ~10 минут долбят -PostgreSQL напрямую через `pgxpool`. Проверяем: Go runtime под нагрузкой, connection pool -под конкурентными запросами, устойчивость кластера. +Новая функция `stress-go-pgstorm` — Go код со 100 горутинами, которые 10 минут +долбят PostgreSQL напрямую через `pgxpool`. Проверяем: Go runtime под нагрузкой, +connection pool под конкурентными запросами, устойчивость кластера. + +**Попутно выявлены и исправлены два системных бага:** +- Хардкодный таймаут 30s в invoke.go (прокси оператора) +- Отсутствие proxy-read-timeout на nginx ingress sless-operator ### Задачи | # | Задача | Статус | Заметки | |---|--------|--------|---------| | 1 | `runtimes/go1.23/go.mod` — добавить `pgx/v5 v5.7.2` | ✅ | go mod tidy на remote, go.sum сгенерирован (28 строк) | -| 2 | `runtimes/go1.23/go.sum` — создан | ✅ | Все indirect deps: pgpassfile, pgservicefile, puddle/v2, crypto, sync, text | -| 3 | `runtimes/go1.23/Dockerfile` — добавить `go mod download` | ⏳ | | -| 4 | Собрать `naeel/sless-runtime-go1.23:v0.1.1`, запушить | ⏳ | | -| 5 | `internal/builder/context.go` — обновить тег `go1.23` → v0.1.1 | ⏳ | | -| 6 | Пересобрать оператор `v0.1.39`, задеплоить | ⏳ | | -| 7 | `code/stress-go-pgstorm/handler.go` — горутины + pgxpool | ⏳ | | -| 8 | `resources.tf` — новая функция + trigger | ⏳ | | -| 9 | `terraform apply`, тест | ⏳ | | -| 10 | Коммит | ⏳ | | +| 2 | `runtimes/go1.23/Dockerfile` — `go mod download` кешируем deps | ✅ | Один stage `golang:1.23-alpine`, COPY go.mod+go.sum перед server.go | +| 3 | Собрать `naeel/sless-runtime-go1.23:v0.1.1`, запушить | ✅ | docker build + push, sha256 verified | +| 4 | `internal/builder/context.go` — тег `go1.23` v0.1.0 → v0.1.1 | ✅ | строка `return "naeel/sless-runtime-go1.23:v0.1.1", nil` | +| 5 | `internal/api/handler/invoke.go` — динамический таймаут | ✅ | Таймаут = Function.Spec.TimeoutSec + 5s (был хардкод 30s) | +| 6 | Пересобрать и задеплоить оператор `v0.1.40` | ✅ | rodlout complete | +| 7 | `deployments/k8s/operator.yaml` — nginx proxy-read-timeout=900s | ✅ | kubectl annotate + манифест обновлён | +| 8 | `code/stress-go-pgstorm/handler.go` — горутины + pgxpool | ✅ | 100 горутин, INSERT/COUNT/MAX, параметры workers/duration_sec/max_delay_ms | +| 9 | `resources.tf` — новая функция + trigger | ✅ | timeout_sec=700, memory_mb=256, все PG env vars | +| 10 | terraform apply — 2 ресурса добавлено | ✅ | apply complete: 2 added | +| 11 | Запуск 10-минутного стресс-теста | ⏳ | Идёт прямо сейчас: workers=100, duration_sec=600, max_delay_ms=300 | +| 12 | Коммит d7fda15 | ✅ | feat: Go runtime v0.1.1 (pgx/v5)... | ### Архитектура функции stress-go-pgstorm ``` Handle(event) → запускает N горутин (default 100) каждая горутина в цикле duration_sec (default 600): - - случайная задержка 0-500ms + - случайная задержка 0-300ms (max_delay_ms) - чередующиеся операции: INSERT / SELECT COUNT / SELECT MAX - - логирует ошибки, считает ok/err - финал → {workers, duration_sec, total_ops, ok_ops, err_ops, ops_per_sec} + - считает ok/err атомарно через sync/atomic + WaitGroup.Wait() → возвращает итог + возвращает: {runtime, version, workers, duration_sec, elapsed_sec, + total_ops, ok_ops, err_ops, ops_per_sec} + pgxpool.New() — connection pool, MaxConns=20 env: PGHOST, PGPORT, PGDATABASE, PGUSER, PGPASSWORD, PGSSLMODE ``` ---- +### Цепочка таймаутов (после фиксов) + +| Слой | Таймаут | Где задаётся | +|------|---------|--------------| +| curl --max-time | 720s | клиент | +| nginx ingress proxy-read-timeout | 900s | аннотация ingress sless-operator | +| operator http.Client (invoke.go) | TimeoutSec+5s = 705s | Function.Spec.TimeoutSec=700 | +| function runtime (Go) | duration_sec = 600s | параметр в JSON body | + +### Что было сломано до этой сессии + +1. invoke.go: `var httpClient = &http.Client{Timeout: 30 * time.Second}` — хардкод в строке 24 +2. nginx ingress sless-operator: аннотации proxy-read-timeout не было → nginx дефолт 60s → 504 ## 2026-03-19 — Tests 3-7: E2E прогон POSTGRES + 8 стресс-функций