doc: Go runtime v0.1.1, баги 4+5, решения pgx/v5 + dynamic timeout
- progress.md: секция v0.1.1 → ✅ ЗАВЕРШЕНО, все задачи, таблица таймаутов, арх. функции
- errors/log.md: Баг 4 (invoke.go 30s хардкод → context deadline exceeded) + Баг 5 (nginx ingress отсутствие proxy-read-timeout → 504)
- decisions/log.md: pgx/v5 vs database/sql+lib/pq (с обоснованием), динамический таймаут (почему +5s, почему не кешировать, деградация)
This commit is contained in:
@@ -861,3 +861,72 @@ for _, k := range keys { envVars = append(envVars, corev1.EnvVar{Name: k, Value:
|
|||||||
|
|
||||||
**Тесты:** 2 теста в `controllers/function_controller_unit_test.go`
|
**Тесты:** 2 теста в `controllers/function_controller_unit_test.go`
|
||||||
(4 env vars → алфавитный порядок после SLESS_ENTRYPOINT; пустой Env → только SLESS_ENTRYPOINT).
|
(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`
|
||||||
|
|||||||
@@ -631,3 +631,127 @@ Attribute runtime value must be one of: ["nodejs20" "python3.11" "go1.21"], got:
|
|||||||
### Версии
|
### Версии
|
||||||
- Оператор: `naeel/sless-operator:v0.1.13`
|
- Оператор: `naeel/sless-operator:v0.1.13`
|
||||||
- Провайдер: `terra.k8c.ru/naeel/sless v0.1.7`
|
- Провайдер: `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
|
||||||
|
<html><head><title>504 Gateway Time-out</title></head>
|
||||||
|
<body><center><h1>504 Gateway Time-out</h1></center>
|
||||||
|
<hr><center>nginx</center></body></html>
|
||||||
|
```
|
||||||
|
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 — обязательны явные таймауты.
|
||||||
|
|||||||
+39
-18
@@ -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 минут долбят
|
Новая функция `stress-go-pgstorm` — Go код со 100 горутинами, которые 10 минут
|
||||||
PostgreSQL напрямую через `pgxpool`. Проверяем: Go runtime под нагрузкой, connection pool
|
долбят 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 строк) |
|
| 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 |
|
| 2 | `runtimes/go1.23/Dockerfile` — `go mod download` кешируем deps | ✅ | Один stage `golang:1.23-alpine`, COPY go.mod+go.sum перед server.go |
|
||||||
| 3 | `runtimes/go1.23/Dockerfile` — добавить `go mod download` | ⏳ | |
|
| 3 | Собрать `naeel/sless-runtime-go1.23:v0.1.1`, запушить | ✅ | docker build + push, sha256 verified |
|
||||||
| 4 | Собрать `naeel/sless-runtime-go1.23:v0.1.1`, запушить | ⏳ | |
|
| 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/builder/context.go` — обновить тег `go1.23` → v0.1.1 | ⏳ | |
|
| 5 | `internal/api/handler/invoke.go` — динамический таймаут | ✅ | Таймаут = Function.Spec.TimeoutSec + 5s (был хардкод 30s) |
|
||||||
| 6 | Пересобрать оператор `v0.1.39`, задеплоить | ⏳ | |
|
| 6 | Пересобрать и задеплоить оператор `v0.1.40` | ✅ | rodlout complete |
|
||||||
| 7 | `code/stress-go-pgstorm/handler.go` — горутины + pgxpool | ⏳ | |
|
| 7 | `deployments/k8s/operator.yaml` — nginx proxy-read-timeout=900s | ✅ | kubectl annotate + манифест обновлён |
|
||||||
| 8 | `resources.tf` — новая функция + trigger | ⏳ | |
|
| 8 | `code/stress-go-pgstorm/handler.go` — горутины + pgxpool | ✅ | 100 горутин, INSERT/COUNT/MAX, параметры workers/duration_sec/max_delay_ms |
|
||||||
| 9 | `terraform apply`, тест | ⏳ | |
|
| 9 | `resources.tf` — новая функция + trigger | ✅ | timeout_sec=700, memory_mb=256, все PG env vars |
|
||||||
| 10 | Коммит | ⏳ | |
|
| 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
|
### Архитектура функции stress-go-pgstorm
|
||||||
|
|
||||||
```
|
```
|
||||||
Handle(event) → запускает N горутин (default 100)
|
Handle(event) → запускает N горутин (default 100)
|
||||||
каждая горутина в цикле duration_sec (default 600):
|
каждая горутина в цикле duration_sec (default 600):
|
||||||
- случайная задержка 0-500ms
|
- случайная задержка 0-300ms (max_delay_ms)
|
||||||
- чередующиеся операции: INSERT / SELECT COUNT / SELECT MAX
|
- чередующиеся операции: INSERT / SELECT COUNT / SELECT MAX
|
||||||
- логирует ошибки, считает ok/err
|
- считает ok/err атомарно через sync/atomic
|
||||||
финал → {workers, duration_sec, total_ops, ok_ops, err_ops, ops_per_sec}
|
WaitGroup.Wait() → возвращает итог
|
||||||
|
возвращает: {runtime, version, workers, duration_sec, elapsed_sec,
|
||||||
|
total_ops, ok_ops, err_ops, ops_per_sec}
|
||||||
|
|
||||||
pgxpool.New() — connection pool, MaxConns=20
|
pgxpool.New() — connection pool, MaxConns=20
|
||||||
env: PGHOST, PGPORT, PGDATABASE, PGUSER, PGPASSWORD, PGSSLMODE
|
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 стресс-функций
|
## 2026-03-19 — Tests 3-7: E2E прогон POSTGRES + 8 стресс-функций
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user